Merge branch 'refs/heads/dev' into cw-1440-new-buy-sell-flow

# Conflicts:
#	lib/new-ui/widgets/coins_page/cards/cards_view.dart
#	lib/new-ui/widgets/receive_page/receive_top_bar.dart
This commit is contained in:
Robert Malikowski
2026-08-07 01:35:54 +02:00
286 changed files with 13997 additions and 6504 deletions
@@ -159,11 +159,6 @@ 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
+5 -3
View File
@@ -176,16 +176,18 @@ jobs:
- name: Rename apk file
run: |
sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
cd build/app/outputs/flutter-apk
mkdir test-apk
cp app-arm64-v8a-release.apk test-apk/${BRANCH_NAME}.apk
cp app-x86_64-release.apk test-apk/${BRANCH_NAME}_x86.apk
cp app-arm64-v8a-release.apk test-apk/${sanitized_branch_name}.apk
cp app-x86_64-release.apk test-apk/${sanitized_branch_name}_x86.apk
- name: Find APK file
id: find_apk
run: |
set -x
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1)
sanitized_branch_name=$(grep '^id=' android/app.properties | cut -d'=' -f2 | sed 's/com\.cakewallet\.test_//')
apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${sanitized_branch_name}.apk || exit 1)
echo "APK_FILE=$apk_file" >> $GITHUB_ENV
- name: Write BRANCH_NAME to env
run: echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV
+2 -1
View File
@@ -226,4 +226,5 @@ scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
*.salive
*.salive
android/.kotlin
-1
View File
@@ -32,7 +32,6 @@ Cake Wallet includes support for several cryptocurrencies, including:
* Nano (XNO)
* Zano (ZANO)
* Decred (DCR)
* Wownero (WOW)
## Features
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

+1 -1
View File
@@ -1,6 +1,5 @@
-
uri: api.trongrid.io
isDefault: true
useSSL: true
isEnabledForAutoSwitching: true
label: "TronGrid"
@@ -10,6 +9,7 @@
label: "PublicNode"
-
uri: trx.nownodes.io
isDefault: true
useSSL: true
isEnabledForAutoSwitching: true
label: "NOWNodes"
+2 -2
View File
@@ -5,8 +5,8 @@
useSSL: true
isEnabledForAutoSwitching: true
label: "Cake Wallet"
-
uri: zec.rocks:443
- uri: zec.rocks:443
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
label: "Zec.rocks"
+6 -2
View File
@@ -17,6 +17,7 @@ import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/lightning/lightning_wallet.dart';
import 'package:cw_bitcoin/hardware/bitcoin_ledger_service.dart';
import 'package:cw_bitcoin/output_ordering.dart';
import 'package:cw_bitcoin/payjoin/manager.dart';
import 'package:cw_bitcoin/payjoin/storage.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
@@ -467,8 +468,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final masterFingerprint =
await (hardwareWalletService as BitcoinHardwareWalletService).getMasterFingerprint();
final orderedOutputs = orderOutputs(outputs, outputOrdering);
final psbt = await buildPsbt(
outputs: outputs,
outputs: orderedOutputs,
fee: fee,
network: network,
utxos: utxos,
@@ -478,7 +481,8 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
memo: memo,
enableRBF: enableRBF,
inputOrdering: inputOrdering,
outputOrdering: outputOrdering,
// Already applied above; don't reorder again.
outputOrdering: BitcoinOrdering.none,
);
final psbtStr = base64Encode(psbt.serialize());
+1 -1
View File
@@ -124,7 +124,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
Future<PaymentURI> getPaymentRequestUri(String amount) async {
if (addressPageType is LightningAddressType && lightningWallet != null) {
final amountSats = amount.isNotEmpty ? parseFixed(amount, 8) : null;
final amountSats = amount.isNotEmpty ? tryParseFixed(amount, 8) : null;
final lnUrl = getLnurlOfLightningAddress(address);
if (amountSats == null) {
return LightningPaymentRequest(address: address, lnURL: lnUrl, amount: amount);
+44 -28
View File
@@ -18,7 +18,6 @@ import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/bitcoin_wallet.dart';
import 'package:cw_bitcoin/bitcoin_wallet_keys.dart';
import 'package:cw_bitcoin/electrum.dart' as electrum;
import 'package:cw_bitcoin/electrum_balance.dart';
@@ -27,25 +26,20 @@ import 'package:cw_bitcoin/electrum_transaction_history.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/litecoin_wallet.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/utils.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/encryption_file_utils.dart';
import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/root_dir.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/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/utils/socket_health_logger.dart';
import 'package:cw_core/utils/tor/abstract.dart';
import 'package:cw_core/wallet_base.dart';
@@ -57,7 +51,6 @@ import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sp_scanner/sp_scanner.dart';
part 'electrum_wallet.g.dart';
@@ -1492,7 +1485,9 @@ abstract class ElectrumWalletBase
fee: estimatedTx.fee.amount,
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
// Shuffle so the change output isn't placed deterministically last
// (privacy fingerprint). Applied by orderOutputs in the builder.
outputOrdering: BitcoinOrdering.shuffle,
enableRBF: true,
cwOutputs: transactionCredentials.outputs,
);
@@ -1525,7 +1520,10 @@ abstract class ElectrumWalletBase
fee: estimatedTx.fee.amount,
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
// Shuffle so the change output isn't placed deterministically last
// (privacy fingerprint). Change is found by isChange, not position.
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.shuffle,
enableRBF: !estimatedTx.spendsUnconfirmedTX,
);
} else {
@@ -1536,7 +1534,7 @@ abstract class ElectrumWalletBase
network: network,
memo: estimatedTx.memo,
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.none,
outputOrdering: BitcoinOrdering.shuffle,
enableRBF: !estimatedTx.spendsUnconfirmedTX,
);
}
@@ -1594,6 +1592,7 @@ abstract class ElectrumWalletBase
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
derivedOutputs: updatedOutputs,
publicKeys: estimatedTx.publicKeys,
isViewOnly: keys.privateKey.isEmpty,
)..addListener((transaction) async {
@@ -1983,8 +1982,12 @@ abstract class ElectrumWalletBase
@action
Future<void> addCoinInfo(BitcoinUnspent coin) async {
// Check if the coin is already in the unspentCoinsInfo for the wallet
final existingCoinInfo = unspentCoinsInfo.values
.firstWhereOrNull((element) => element.walletId == walletInfo.id && element == coin);
final existingCoinInfo = unspentCoinsInfo.values.firstWhereOrNull(
(element) =>
element.walletId == walletInfo.id &&
element.hash == coin.hash &&
element.vout == coin.vout,
);
if (existingCoinInfo == null) {
final newInfo = UnspentCoinsInfo(
@@ -2297,7 +2300,7 @@ abstract class ElectrumWalletBase
network: network,
memo: memo,
inputOrdering: BitcoinOrdering.shuffle,
outputOrdering: BitcoinOrdering.none,
outputOrdering: BitcoinOrdering.shuffle,
enableRBF: true,
);
@@ -2509,10 +2512,16 @@ abstract class ElectrumWalletBase
Map<String, ElectrumTransactionInfo> historiesWithDetails,
BitcoinAddressType type,
) async {
final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type);
final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true);
final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false);
walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
final addressesByType =
walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
await walletAddresses.saveAddressesInBox();
await Future.wait(addressesByType.map((addressRecord) async {
final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip());
@@ -2521,7 +2530,9 @@ abstract class ElectrumWalletBase
addressRecord.txCount = history.length;
historiesWithDetails.addAll(history);
final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses;
final matchedAddresses = addressRecord.isHidden
? (addressRecord.isLegacyDerivation ? changeLegacy : changeStandard)
: (addressRecord.isLegacyDerivation ? receiveLegacy : receiveStandard);
final isUsedAddressAboveGap = matchedAddresses.toList().indexOf(addressRecord) >=
matchedAddresses.length -
(addressRecord.isHidden
@@ -2625,18 +2636,19 @@ abstract class ElectrumWalletBase
Future<void> fetchTransactionsForAddressTypeBatch(
Map<String, ElectrumTransactionInfo> historiesWithDetails, BitcoinAddressType type) async {
final addressesByType =
walletAddresses.allAddresses.where((addr) => addr.type == type).toList();
final receiveAddresses = addressesByType.where((addr) => !addr.isHidden).toList();
final hiddenAddresses = addressesByType.where((addr) => addr.isHidden).toList();
walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address));
final receiveStandard = getAddressBranchByType(hidden: false, legacy: false, type: type);
final changeStandard = getAddressBranchByType(hidden: true, legacy: false, type: type);
final receiveLegacy = getAddressBranchByType(hidden: false, legacy: true, type: type);
final changeLegacy = getAddressBranchByType(hidden: true, legacy: true, type: type);
walletAddresses.hiddenAddresses.addAll([...changeStandard, ...changeLegacy].map((e) => e.address));
await walletAddresses.saveAddressesInBox();
await fetchTransactionsForAddressesBranchBatch(
historiesWithDetails,
type,
receiveAddresses,
receiveStandard,
isHidden: false,
isLegacyDerivation: false,
);
@@ -2644,7 +2656,7 @@ abstract class ElectrumWalletBase
await fetchTransactionsForAddressesBranchBatch(
historiesWithDetails,
type,
hiddenAddresses,
changeStandard,
isHidden: true,
isLegacyDerivation: false,
);
@@ -2652,7 +2664,7 @@ abstract class ElectrumWalletBase
await fetchTransactionsForAddressesBranchBatch(
historiesWithDetails,
type,
receiveAddresses,
receiveLegacy,
isHidden: false,
isLegacyDerivation: true,
);
@@ -2660,7 +2672,7 @@ abstract class ElectrumWalletBase
await fetchTransactionsForAddressesBranchBatch(
historiesWithDetails,
type,
hiddenAddresses,
changeLegacy,
isHidden: true,
isLegacyDerivation: true,
);
@@ -2736,6 +2748,11 @@ abstract class ElectrumWalletBase
}
}
List<BitcoinAddressRecord> getAddressBranchByType({required bool hidden, required bool legacy, required BitcoinAddressType
type}) => walletAddresses.allAddresses.where((addr) => addr.type == type && addr.isHidden == hidden && addr.isLegacyDerivation == legacy)
.toList()
..sort((a, b) => a.index.compareTo(b.index));
int _highestUsedIndex(List<BitcoinAddressRecord> addresses) {
for (int i = addresses.length - 1; i >= 0; i--) {
if (addresses[i].isUsed) return i;
@@ -3680,7 +3697,6 @@ abstract class ElectrumWalletBase
syncStatus = ConnectingSyncStatus();
}
break;
default:
}
}
+5 -1
View File
@@ -30,6 +30,7 @@ import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_bitcoin/hardware/bitcoin_hardware_wallet_service.dart';
import 'package:cw_bitcoin/litecoin_wallet_addresses.dart';
import 'package:cw_bitcoin/output_ordering.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/psbt/transaction_builder.dart';
import 'package:cw_bitcoin/utils.dart';
@@ -1584,8 +1585,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
));
}
final orderedOutputs = orderOutputs(outputs, outputOrdering);
final rawHex = await (hardwareWalletService as LitecoinHardwareWalletService)
.signLitecoinTransaction(outputs: outputs, inputs: readyInputs, publicKeys: publicKeys);
.signLitecoinTransaction(
outputs: orderedOutputs, inputs: readyInputs, publicKeys: publicKeys);
return BtcTransaction.fromRaw(rawHex);
}
+21
View File
@@ -0,0 +1,21 @@
import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart';
/// Applies [ordering] to a transaction's [outputs] before building.
///
/// Returns a new list; the input is never mutated. Only [BitcoinOrdering.shuffle]
/// reorders (using a secure RNG by default) so the change output is no longer
/// placed deterministically last. Any other value preserves the given order,
/// matching the pre-existing hardware-wallet behavior.
List<T> orderOutputs<T>(
List<T> outputs,
BitcoinOrdering ordering, {
Random? rng,
}) {
final result = List<T>.of(outputs);
if (ordering == BitcoinOrdering.shuffle) {
result.shuffle(rng ?? Random.secure());
}
return result;
}
@@ -31,6 +31,7 @@ class PendingBitcoinTransaction with PendingTransaction {
this.hasTaprootInputs = false,
this.isMweb = false,
this.utxos = const [],
this.derivedOutputs = const [],
this.publicKeys,
this.commitOverride,
this.unsignedPsbt,
@@ -47,6 +48,9 @@ class PendingBitcoinTransaction with PendingTransaction {
final bool hasTaprootInputs;
final bool isViewOnly;
List<UtxoWithAddress> utxos;
final List<BitcoinOutput> derivedOutputs;
bool isMweb;
String? changeAddressOverride;
String? idOverride;
@@ -79,6 +83,13 @@ class PendingBitcoinTransaction with PendingTransaction {
bool get hasSilentPayment => _tx.hasSilentPayment;
List<String?> get stealthAddresses => derivedOutputs
.where((out) => !out.isChange)
.map((out) => out.isSilentPayment
? out.address.toAddress(network ?? BitcoinNetwork.mainnet)
: null)
.toList();
PendingChange? get change {
try {
final change = _tx.outputs.firstWhere((out) => out.isChange);
+5 -5
View File
@@ -634,10 +634,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -1100,10 +1100,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
@@ -1322,5 +1322,5 @@ packages:
source: hosted
version: "2.2.2"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.29.0"
+52
View File
@@ -0,0 +1,52 @@
import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/output_ordering.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('orderOutputs', () {
test('none preserves the given order', () {
final outputs = [0, 1, 2, 3, 4];
expect(orderOutputs(outputs, BitcoinOrdering.none), outputs);
});
test('does not mutate the input list', () {
final outputs = [0, 1, 2, 3, 4];
orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(1));
expect(outputs, [0, 1, 2, 3, 4]);
});
test('returns a new list instance', () {
final outputs = [0, 1, 2];
expect(identical(orderOutputs(outputs, BitcoinOrdering.none), outputs), isFalse);
});
test('shuffle preserves the multiset of outputs', () {
final outputs = [0, 1, 2, 3, 4, 5, 6, 7];
final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(7));
expect(shuffled.length, outputs.length);
expect(shuffled.toSet(), outputs.toSet());
});
test('shuffle is deterministic for a given seed', () {
final outputs = [0, 1, 2, 3, 4, 5];
final a = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42));
final b = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(42));
expect(a, b);
});
test('shuffle does not keep the change output deterministically last', () {
// Last element (99) models the change output, appended last today.
final outputs = [0, 1, 2, 3, 99];
final changePositions = <int>{};
for (var seed = 0; seed < 50; seed++) {
final shuffled = orderOutputs(outputs, BitcoinOrdering.shuffle, rng: Random(seed));
changePositions.add(shuffled.indexOf(99));
}
// Across seeds the change lands in more than one position, and not always last.
expect(changePositions.length, greaterThan(1));
expect(changePositions, isNot(equals({outputs.length - 1})));
});
});
}
+54 -22
View File
@@ -1,13 +1,12 @@
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/format_fixed.dart';
import 'package:cw_core/parse_fixed.dart';
import "package:cw_core/crypto_amount_format.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency.dart";
import "package:cw_core/format_fixed.dart";
import "package:cw_core/parse_fixed.dart";
export "money_local.dart";
class Money implements Comparable<Money> {
final BigInt amount;
final Currency currency;
const Money(this.amount, this.currency);
factory Money.zero(Currency currency) => Money(BigInt.zero, currency);
@@ -38,6 +37,9 @@ class Money implements Comparable<Money> {
return amount != null ? Money(amount, currency) : null;
}
final BigInt amount;
final Currency currency;
/// Returns the sign of this [BigInt] amount.
/// Returns 0 for zero, -1 for values less than zero and +1 for values
/// greater than zero.
@@ -139,7 +141,9 @@ class Money implements Comparable<Money> {
///
/// The result is again [Money].
Money operator /(BigInt other) {
if (other == BigInt.zero) throw Exception('Division by zero.');
if (other == BigInt.zero) {
throw Exception("Division by zero.");
}
final neg = (amount.isNegative) ^ (other.isNegative);
final A = amount.abs();
@@ -163,7 +167,9 @@ class Money implements Comparable<Money> {
Money copyWith({BigInt? amount, Currency? currency}) {
if (currency != null && amount == null && currency.decimals != this.currency.decimals) {
return Money(
_transformAmount(this.amount, this.currency.decimals, currency.decimals), currency);
_transformAmount(this.amount, this.currency.decimals, currency.decimals),
currency,
);
}
return Money(amount ?? this.amount, currency ?? this.currency);
@@ -173,16 +179,21 @@ class Money implements Comparable<Money> {
Money _withAmount(BigInt amount) => Money(amount, currency);
void _assertSameCurrency(Money other, [String? message]) {
if (currency != other.currency)
if (currency != other.currency) {
throw ArgumentError(message ?? "Cannot operate with money values in different currencies.");
}
}
BigInt _transformAmount(BigInt source, int sourceDecimals, int targetDecimals) {
if (sourceDecimals == targetDecimals) return source;
if (sourceDecimals == targetDecimals) {
return source;
}
if (sourceDecimals > targetDecimals) {
return parseFixed(
formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals), targetDecimals);
formatFixed(source, sourceDecimals).withMaxDecimals(targetDecimals),
targetDecimals,
);
} else {
return parseFixed(formatFixed(source, sourceDecimals), targetDecimals);
}
@@ -198,18 +209,39 @@ class Money implements Comparable<Money> {
@override
String toString() => formatFixed(amount, currency.decimals);
String toStringWithSymbol(
{int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) =>
"${toStringWithPrecision(fractionalDigits: fractionalDigits, trimZeros: trimZeros, useBaseUnit: useBaseUnit)} ${_getSymbol(useBaseUnit)}";
String toStringWithSymbol({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
bool withSymbolPrefix = false,
}) {
final amount = toStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
);
final symbol = getSymbol(useBaseUnit: useBaseUnit);
String toStringWithPrecision(
{int? fractionalDigits, bool trimZeros = true, bool useBaseUnit = false}) =>
formatFixed(amount, useBaseUnit ? 0 : currency.decimals,
fractionalDigits: fractionalDigits, trimZeros: trimZeros);
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
}
String toStringWithPrecision({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
}) =>
formatFixed(
amount,
useBaseUnit ? 0 : currency.decimals,
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
);
// To Override the symbol with the ticker of the base unit
String _getSymbol(bool useBaseUnit) {
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) return "sats";
String getSymbol({required bool useBaseUnit}) {
if (useBaseUnit && [CryptoCurrency.btc, CryptoCurrency.btcln].contains(currency)) {
return "sats";
}
return currency.symbol;
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
import 'dart:math';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/currency.dart';
import "dart:math";
import "package:cw_core/amount/money.dart";
import "package:cw_core/currency.dart";
extension ToMoney on double {
/// Turn a double representation of a currency amount to a proper Money representation
+47
View File
@@ -0,0 +1,47 @@
import "package:cw_core/amount/money.dart";
import "package:intl/intl.dart";
extension WithLocalSeparator on Money {
String toLocalStringWithSymbol({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
bool withSymbolPrefix = false,
String? locale,
}) {
final amount = toLocalStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
locale: locale,
);
final symbol = getSymbol(useBaseUnit: useBaseUnit);
return withSymbolPrefix ? "$symbol $amount" : "$amount $symbol";
}
String toLocalStringWithPrecision({
int? fractionalDigits,
bool trimZeros = true,
bool useBaseUnit = false,
String? locale,
}) =>
_withLocalSeparator(
toStringWithPrecision(
fractionalDigits: fractionalDigits,
trimZeros: trimZeros,
useBaseUnit: useBaseUnit,
),
locale: locale,
);
String _withLocalSeparator(String amount, {String? locale}) {
final isNegative = amount.startsWith("-");
final formater = NumberFormat("#,###", locale);
final parts = (isNegative ? amount.substring(1) : amount).split(".");
final formatted = [formater.format(int.tryParse(parts.first) ?? 0), ...parts.sublist(1)]
.join(formater.symbols.DECIMAL_SEP);
return isNegative ? "-$formatted" : formatted;
}
}
+416
View File
@@ -0,0 +1,416 @@
class ZcashHeight {
// for i in $(seq 1 331); do echo "_Checkpoint($(date -j -f "%Y-%m-%d %H:%M:%S" "$(curl -s "https://mainnet.zcashexplorer.app/blocks/${i}0000" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}')" "+%s"), ${i}0000),"; done > points.txt
static final List<_Checkpoint> _points = [
_Checkpoint(1479114667, 10000),
_Checkpoint(1480618460, 20000),
_Checkpoint(1482129143, 30000),
_Checkpoint(1483635610, 40000),
_Checkpoint(1485145514, 50000),
_Checkpoint(1486655742, 60000),
_Checkpoint(1488163642, 70000),
_Checkpoint(1489668244, 80000),
_Checkpoint(1491175107, 90000),
_Checkpoint(1492681094, 100000),
_Checkpoint(1494185626, 110000),
_Checkpoint(1495691260, 120000),
_Checkpoint(1497196465, 130000),
_Checkpoint(1498702428, 140000),
_Checkpoint(1500209116, 150000),
_Checkpoint(1501716823, 160000),
_Checkpoint(1503222893, 170000),
_Checkpoint(1504730866, 180000),
_Checkpoint(1506236963, 190000),
_Checkpoint(1507744233, 200000),
_Checkpoint(1509254578, 210000),
_Checkpoint(1510768449, 220000),
_Checkpoint(1512275502, 230000),
_Checkpoint(1513783951, 240000),
_Checkpoint(1515292290, 250000),
_Checkpoint(1516799219, 260000),
_Checkpoint(1518306180, 270000),
_Checkpoint(1519813817, 280000),
_Checkpoint(1521319150, 290000),
_Checkpoint(1522828107, 300000),
_Checkpoint(1524336223, 310000),
_Checkpoint(1525844954, 320000),
_Checkpoint(1527354342, 330000),
_Checkpoint(1528860301, 340000),
_Checkpoint(1530365206, 350000),
_Checkpoint(1531873693, 360000),
_Checkpoint(1533380392, 370000),
_Checkpoint(1534888873, 380000),
_Checkpoint(1536393381, 390000),
_Checkpoint(1537899117, 400000),
_Checkpoint(1539406330, 410000),
_Checkpoint(1540912214, 420000),
_Checkpoint(1542422891, 430000),
_Checkpoint(1543931111, 440000),
_Checkpoint(1545437758, 450000),
_Checkpoint(1546945529, 460000),
_Checkpoint(1548450107, 470000),
_Checkpoint(1549957767, 480000),
_Checkpoint(1551466960, 490000),
_Checkpoint(1552969806, 500000),
_Checkpoint(1554477648, 510000),
_Checkpoint(1555981451, 520000),
_Checkpoint(1557489241, 530000),
_Checkpoint(1558997282, 540000),
_Checkpoint(1560503840, 550000),
_Checkpoint(1562011717, 560000),
_Checkpoint(1563518091, 570000),
_Checkpoint(1565027136, 580000),
_Checkpoint(1566532760, 590000),
_Checkpoint(1568039395, 600000),
_Checkpoint(1569547911, 610000),
_Checkpoint(1571051853, 620000),
_Checkpoint(1572559831, 630000),
_Checkpoint(1574069743, 640000),
_Checkpoint(1575576895, 650000),
_Checkpoint(1576603014, 660000),
_Checkpoint(1577356346, 670000),
_Checkpoint(1578109375, 680000),
_Checkpoint(1578863180, 690000),
_Checkpoint(1579616443, 700000),
_Checkpoint(1580369806, 710000),
_Checkpoint(1581122786, 720000),
_Checkpoint(1581876634, 730000),
_Checkpoint(1582630471, 740000),
_Checkpoint(1583383678, 750000),
_Checkpoint(1584133944, 760000),
_Checkpoint(1584887940, 770000),
_Checkpoint(1585641057, 780000),
_Checkpoint(1586395543, 790000),
_Checkpoint(1587148500, 800000),
_Checkpoint(1587901493, 810000),
_Checkpoint(1588655183, 820000),
_Checkpoint(1589408476, 830000),
_Checkpoint(1590163464, 840000),
_Checkpoint(1590916379, 850000),
_Checkpoint(1591669786, 860000),
_Checkpoint(1592423887, 870000),
_Checkpoint(1593176771, 880000),
_Checkpoint(1593930930, 890000),
_Checkpoint(1594684380, 900000),
_Checkpoint(1595439209, 910000),
_Checkpoint(1596192463, 920000),
_Checkpoint(1596946009, 930000),
_Checkpoint(1597700256, 940000),
_Checkpoint(1598453538, 950000),
_Checkpoint(1599207483, 960000),
_Checkpoint(1599960598, 970000),
_Checkpoint(1600714563, 980000),
_Checkpoint(1601467884, 990000),
_Checkpoint(1602220941, 1000000),
_Checkpoint(1602974722, 1010000),
_Checkpoint(1603728848, 1020000),
_Checkpoint(1604485637, 1030000),
_Checkpoint(1605238381, 1040000),
_Checkpoint(1605992807, 1050000),
_Checkpoint(1606747750, 1060000),
_Checkpoint(1607500606, 1070000),
_Checkpoint(1608255278, 1080000),
_Checkpoint(1609008684, 1090000),
_Checkpoint(1609763782, 1100000),
_Checkpoint(1610517871, 1110000),
_Checkpoint(1611271575, 1120000),
_Checkpoint(1612025546, 1130000),
_Checkpoint(1612779940, 1140000),
_Checkpoint(1613533993, 1150000),
_Checkpoint(1614287669, 1160000),
_Checkpoint(1615042297, 1170000),
_Checkpoint(1615792993, 1180000),
_Checkpoint(1616546145, 1190000),
_Checkpoint(1617299867, 1200000),
_Checkpoint(1618054038, 1210000),
_Checkpoint(1618808513, 1220000),
_Checkpoint(1619563243, 1230000),
_Checkpoint(1620318159, 1240000),
_Checkpoint(1621071298, 1250000),
_Checkpoint(1621826156, 1260000),
_Checkpoint(1622580237, 1270000),
_Checkpoint(1623335553, 1280000),
_Checkpoint(1624090141, 1290000),
_Checkpoint(1624844712, 1300000),
_Checkpoint(1625597720, 1310000),
_Checkpoint(1626351652, 1320000),
_Checkpoint(1627106054, 1330000),
_Checkpoint(1627860648, 1340000),
_Checkpoint(1628614015, 1350000),
_Checkpoint(1629368398, 1360000),
_Checkpoint(1630122679, 1370000),
_Checkpoint(1630877190, 1380000),
_Checkpoint(1631631884, 1390000),
_Checkpoint(1632385851, 1400000),
_Checkpoint(1633141006, 1410000),
_Checkpoint(1633895592, 1420000),
_Checkpoint(1634649550, 1430000),
_Checkpoint(1635403121, 1440000),
_Checkpoint(1636157669, 1450000),
_Checkpoint(1636915133, 1460000),
_Checkpoint(1637670036, 1470000),
_Checkpoint(1638424121, 1480000),
_Checkpoint(1639177413, 1490000),
_Checkpoint(1639931234, 1500000),
_Checkpoint(1640684476, 1510000),
_Checkpoint(1641438827, 1520000),
_Checkpoint(1642192402, 1530000),
_Checkpoint(1642946383, 1540000),
_Checkpoint(1643700901, 1550000),
_Checkpoint(1644455522, 1560000),
_Checkpoint(1645208504, 1570000),
_Checkpoint(1645962081, 1580000),
_Checkpoint(1646715150, 1590000),
_Checkpoint(1647465727, 1600000),
_Checkpoint(1648219045, 1610000),
_Checkpoint(1648973131, 1620000),
_Checkpoint(1649727422, 1630000),
_Checkpoint(1650481408, 1640000),
_Checkpoint(1651234287, 1650000),
_Checkpoint(1651989865, 1660000),
_Checkpoint(1652743327, 1670000),
_Checkpoint(1653497455, 1680000),
_Checkpoint(1654252094, 1690000),
_Checkpoint(1655005944, 1700000),
_Checkpoint(1655759708, 1710000),
_Checkpoint(1656513765, 1720000),
_Checkpoint(1657268358, 1730000),
_Checkpoint(1658022453, 1740000),
_Checkpoint(1658777019, 1750000),
_Checkpoint(1659531133, 1760000),
_Checkpoint(1660283907, 1770000),
_Checkpoint(1661037645, 1780000),
_Checkpoint(1661791977, 1790000),
_Checkpoint(1662544394, 1800000),
_Checkpoint(1663298018, 1810000),
_Checkpoint(1664051450, 1820000),
_Checkpoint(1664805547, 1830000),
_Checkpoint(1665558994, 1840000),
_Checkpoint(1666312456, 1850000),
_Checkpoint(1667066473, 1860000),
_Checkpoint(1667823393, 1870000),
_Checkpoint(1668576623, 1880000),
_Checkpoint(1669330657, 1890000),
_Checkpoint(1670084156, 1900000),
_Checkpoint(1670837831, 1910000),
_Checkpoint(1671591094, 1920000),
_Checkpoint(1672343975, 1930000),
_Checkpoint(1673097703, 1940000),
_Checkpoint(1673852016, 1950000),
_Checkpoint(1674605810, 1960000),
_Checkpoint(1675360050, 1970000),
_Checkpoint(1676113710, 1980000),
_Checkpoint(1676867174, 1990000),
_Checkpoint(1677620242, 2000000),
_Checkpoint(1678374852, 2010000),
_Checkpoint(1679125063, 2020000),
_Checkpoint(1679878121, 2030000),
_Checkpoint(1680632254, 2040000),
_Checkpoint(1681386518, 2050000),
_Checkpoint(1682140381, 2060000),
_Checkpoint(1682894564, 2070000),
_Checkpoint(1683648178, 2080000),
_Checkpoint(1684401932, 2090000),
_Checkpoint(1685156464, 2100000),
_Checkpoint(1685910040, 2110000),
_Checkpoint(1686663406, 2120000),
_Checkpoint(1687417875, 2130000),
_Checkpoint(1688172069, 2140000),
_Checkpoint(1688925610, 2150000),
_Checkpoint(1689679500, 2160000),
_Checkpoint(1690432691, 2170000),
_Checkpoint(1691186349, 2180000),
_Checkpoint(1691940901, 2190000),
_Checkpoint(1692694780, 2200000),
_Checkpoint(1693449743, 2210000),
_Checkpoint(1694203665, 2220000),
_Checkpoint(1694957643, 2230000),
_Checkpoint(1695710712, 2240000),
_Checkpoint(1696465787, 2250000),
_Checkpoint(1697218701, 2260000),
_Checkpoint(1697973031, 2270000),
_Checkpoint(1698726864, 2280000),
_Checkpoint(1699483348, 2290000),
_Checkpoint(1700237650, 2300000),
_Checkpoint(1700991210, 2310000),
_Checkpoint(1701745742, 2320000),
_Checkpoint(1702500723, 2330000),
_Checkpoint(1703253121, 2340000),
_Checkpoint(1704007238, 2350000),
_Checkpoint(1704760935, 2360000),
_Checkpoint(1705514669, 2370000),
_Checkpoint(1706268087, 2380000),
_Checkpoint(1707021423, 2390000),
_Checkpoint(1707776257, 2400000),
_Checkpoint(1708529542, 2410000),
_Checkpoint(1709283065, 2420000),
_Checkpoint(1710037165, 2430000),
_Checkpoint(1710787780, 2440000),
_Checkpoint(1711541488, 2450000),
_Checkpoint(1712294996, 2460000),
_Checkpoint(1713048843, 2470000),
_Checkpoint(1713803486, 2480000),
_Checkpoint(1714557127, 2490000),
_Checkpoint(1715311181, 2500000),
_Checkpoint(1716064271, 2510000),
_Checkpoint(1716818792, 2520000),
_Checkpoint(1717572765, 2530000),
_Checkpoint(1718326333, 2540000),
_Checkpoint(1719080275, 2550000),
_Checkpoint(1719834297, 2560000),
_Checkpoint(1720588042, 2570000),
_Checkpoint(1721341832, 2580000),
_Checkpoint(1722095122, 2590000),
_Checkpoint(1722848604, 2600000),
_Checkpoint(1723602357, 2610000),
_Checkpoint(1724355843, 2620000),
_Checkpoint(1725110085, 2630000),
_Checkpoint(1725863681, 2640000),
_Checkpoint(1726616892, 2650000),
_Checkpoint(1727371443, 2660000),
_Checkpoint(1728124893, 2670000),
_Checkpoint(1728878141, 2680000),
_Checkpoint(1729631572, 2690000),
_Checkpoint(1730385888, 2700000),
_Checkpoint(1731142437, 2710000),
_Checkpoint(1731896266, 2720000),
_Checkpoint(1732650193, 2730000),
_Checkpoint(1733403626, 2740000),
_Checkpoint(1734157390, 2750000),
_Checkpoint(1734911442, 2760000),
_Checkpoint(1735665720, 2770000),
_Checkpoint(1736419845, 2780000),
_Checkpoint(1737173511, 2790000),
_Checkpoint(1737926795, 2800000),
_Checkpoint(1738681511, 2810000),
_Checkpoint(1739434236, 2820000),
_Checkpoint(1740188214, 2830000),
_Checkpoint(1740941352, 2840000),
_Checkpoint(1741692338, 2850000),
_Checkpoint(1742445247, 2860000),
_Checkpoint(1743198927, 2870000),
_Checkpoint(1743952947, 2880000),
_Checkpoint(1744706370, 2890000),
_Checkpoint(1745461377, 2900000),
_Checkpoint(1746214735, 2910000),
_Checkpoint(1746967908, 2920000),
_Checkpoint(1747722271, 2930000),
_Checkpoint(1748474582, 2940000),
_Checkpoint(1749228582, 2950000),
_Checkpoint(1749983372, 2960000),
_Checkpoint(1750737278, 2970000),
_Checkpoint(1751490762, 2980000),
_Checkpoint(1752243931, 2990000),
_Checkpoint(1752997873, 3000000),
_Checkpoint(1753751756, 3010000),
_Checkpoint(1754504935, 3020000),
_Checkpoint(1755259449, 3030000),
_Checkpoint(1756012904, 3040000),
_Checkpoint(1756765335, 3050000),
_Checkpoint(1757519206, 3060000),
_Checkpoint(1758272484, 3070000),
_Checkpoint(1759025597, 3080000),
_Checkpoint(1759778957, 3090000),
_Checkpoint(1760532780, 3100000),
_Checkpoint(1761286210, 3110000),
_Checkpoint(1762039783, 3120000),
_Checkpoint(1762796627, 3130000),
_Checkpoint(1763550532, 3140000),
_Checkpoint(1764303860, 3150000),
_Checkpoint(1765057381, 3160000),
_Checkpoint(1765811222, 3170000),
_Checkpoint(1766564356, 3180000),
_Checkpoint(1767318326, 3190000),
_Checkpoint(1768071956, 3200000),
_Checkpoint(1768824957, 3210000),
_Checkpoint(1769578781, 3220000),
_Checkpoint(1770332206, 3230000),
_Checkpoint(1771086265, 3240000),
_Checkpoint(1771840180, 3250000),
_Checkpoint(1772592783, 3260000),
_Checkpoint(1773343947, 3270000),
_Checkpoint(1774097085, 3280000),
_Checkpoint(1774850161, 3290000),
_Checkpoint(1775603525, 3300000),
_Checkpoint(1776358117, 3310000),
];
static int getBlockHeightByTime(DateTime time) {
final t = time.toUtc().millisecondsSinceEpoch;
if (_points.isEmpty) return 0;
if (t <= _points.first.timeMs) {
return 1;
}
for (int i = 0; i < _points.length - 1; i++) {
final p0 = _points[i];
final p1 = _points[i + 1];
if (t >= p0.timeMs && t < p1.timeMs) {
return _interpolate(
p0.height,
p1.height,
p0.timeMs,
p1.timeMs,
t,
);
}
}
final last = _points[_points.length - 1];
final prev = _points[_points.length - 2];
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
final blocksSinceLast = ((t - last.timeMs) / avgBlockTimeMs).floor();
return last.height + blocksSinceLast;
}
static DateTime getTimeByBlockHeight(int height) {
if (_points.isEmpty) return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
if (height <= 1) {
return DateTime.fromMillisecondsSinceEpoch(_points.first.timeMs, isUtc: true);
}
for (int i = 0; i < _points.length - 1; i++) {
final p0 = _points[i];
final p1 = _points[i + 1];
if (height >= p0.height && height < p1.height) {
final t = _interpolate(
p0.timeMs,
p1.timeMs,
p0.height,
p1.height,
height,
);
return DateTime.fromMillisecondsSinceEpoch(t, isUtc: true);
}
}
final last = _points[_points.length - 1];
final prev = _points[_points.length - 2];
final avgBlockTimeMs = (last.timeMs - prev.timeMs) / (last.height - prev.height);
final msSinceLast = ((height - last.height) * avgBlockTimeMs).floor();
return DateTime.fromMillisecondsSinceEpoch(last.timeMs + msSinceLast, isUtc: true);
}
static int _interpolate(
int h0,
int h1,
int t0,
int t1,
int t,
) {
final ratio = (t - t0) / (t1 - t0);
return h0 + ((h1 - h0) * ratio).floor();
}
}
class _Checkpoint {
final int timeMs;
final int height;
_Checkpoint(int timeSec, this.height) : timeMs = timeSec * 1000;
}
+140 -37
View File
@@ -1,19 +1,20 @@
import 'dart:convert';
import "dart:convert";
import 'package:bech32/bech32.dart';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import "package:bech32/bech32.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/utils/proxy_wrapper.dart";
const _BOLT_PREFIXES = ["lnbcrt", "lntbs", "lnbc", "lntb"];
const _LUD17_PREFIXES = ['lnurlw', 'lnurlc', 'lnurlp', 'keyauth'];
const _LUD17_PREFIXES = ["lnurlw", "lnurlc", "lnurlp", "keyauth"];
bool isBolt11ZeroInvoice(String invoice) {
try {
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final request =
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final prefix =
_BOLT_PREFIXES.firstWhere((prefix) => request.hrp.startsWith(prefix), orElse: () => "");
_BOLT_PREFIXES.firstWhere(request.hrp.startsWith, orElse: () => "");
return request.hrp.length == prefix.length;
} catch (e) {
@@ -23,48 +24,56 @@ bool isBolt11ZeroInvoice(String invoice) {
/// Get the amount of a Bolt 11 Invoice in
int? _getAmountBolt11Msat(String invoice) {
final request = Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final request =
const Bech32Codec().decode(invoice.replaceFirst("lightning:", ""), invoice.length);
final prefix = _BOLT_PREFIXES.firstWhere(
(prefix) => request.hrp.startsWith(prefix),
orElse: () => throw FormatException('Invalid BOLT11 invoice: unknown HRP prefix.'),
request.hrp.startsWith,
orElse: () => throw const FormatException("Invalid BOLT11 invoice: unknown HRP prefix."),
);
final amountPart = request.hrp.substring(prefix.length);
if (amountPart.isEmpty) return null; // zero-amount invoice
if (amountPart.isEmpty) {
return null; // zero-amount invoice
}
final hasMultiplier = RegExp(r'[munp]$').hasMatch(amountPart);
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : '';
final hasMultiplier = RegExp(r"[munp]$").hasMatch(amountPart);
final multiplier = hasMultiplier ? amountPart[amountPart.length - 1] : "";
final numberStr = hasMultiplier ? amountPart.substring(0, amountPart.length - 1) : amountPart;
if (numberStr.isEmpty || !RegExp(r'^\d+$').hasMatch(numberStr)) {
throw FormatException('Invalid BOLT11 invoice: invalid amount number in HRP.');
if (numberStr.isEmpty || !RegExp(r"^\d+$").hasMatch(numberStr)) {
throw const FormatException("Invalid BOLT11 invoice: invalid amount number in HRP.");
}
final amount = int.parse(numberStr);
switch (multiplier) {
case '': // bitcoin
case "": // bitcoin
return amount * 100000000000;
case 'm': // milli-bitcoin
case "m": // milli-bitcoin
return amount * 100000000;
case 'u': // micro-bitcoin
case "u": // micro-bitcoin
return amount * 100000;
case 'n': // nano-bitcoin
case "n": // nano-bitcoin
return amount * 100;
case 'p':
case "p":
if (amount % 10 != 0) {
throw FormatException('Invalid BOLT11 invoice: amount not representable in whole msat.');
throw const FormatException(
"Invalid BOLT11 invoice: amount not representable in whole msat.",
);
}
return amount ~/ 10;
default:
throw FormatException('Invalid BOLT11 invoice: unknown amount multiplier.');
throw const FormatException("Invalid BOLT11 invoice: unknown amount multiplier.");
}
}
Money? getBolt11Amount(String invoice) {
final msat = _getAmountBolt11Msat(invoice);
if (msat == null || msat % 1000 != 0) return null;
if (msat == null || msat % 1000 != 0) {
return null;
}
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
}
@@ -73,30 +82,99 @@ class LNURL {
final url = decode(lnurl);
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode != 200) return null;
if (response.statusCode != 200) {
return null;
}
try {
final body = jsonDecode(response.body) as Map;
final tag = body["tag"] as String?;
if (tag != "payRequest") return null;
if (tag != "payRequest") {
return null;
}
final msat = body["minSendable"] as int?;
final maxSendable = body["maxSendable"] as int?;
// if minSendable and maxSendable are the same we assume a specific payment request
if (msat != maxSendable) return null;
if (msat != maxSendable) {
return null;
}
if (msat == null || msat % 1000 != 0) return null;
if (msat == null || msat % 1000 != 0) {
return null;
}
return Money.fromInt(msat ~/ 1000, CryptoCurrency.btcln);
} catch (_) {
return null;
}
}
static Future<LNURLWithdrawRequest?> getWithdrawRequest(String lnurl) async {
final url = decode(lnurl);
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode != 200) {
return null;
}
try {
final body = jsonDecode(response.body) as Map;
final tag = body["tag"] as String?;
if (tag != "withdrawRequest") {
return null;
}
final minWithdrawable = body["minWithdrawable"] as int?;
final maxWithdrawable = body["maxWithdrawable"] as int?;
if ((minWithdrawable == null || minWithdrawable % 1000 != 0) ||
(maxWithdrawable == null || maxWithdrawable % 1000 != 0)) {
return null;
}
return LNURLWithdrawRequest(
callback: body["callback"] as String,
k1: body["k1"] as String,
description: body["defaultDescription"] as String,
minAmount: Money.fromInt(minWithdrawable ~/ 1000, CryptoCurrency.btcln),
maxAmount: Money.fromInt(maxWithdrawable ~/ 1000, CryptoCurrency.btcln),
);
} catch (_) {
return null;
}
}
Future<bool> commitWithdrawRequest(LNURLWithdrawRequest request, String invoice) async {
try {
final url = Uri.parse(request.callback);
url.queryParameters["k1"] = request.k1;
url.queryParameters["pr"] = invoice;
final response = await ProxyWrapper().get(clearnetUri: url);
if (response.statusCode != 200) {
return false;
}
final body = jsonDecode(response.body) as Map;
final status = body["status"] as String?;
if (status == "OK") {
return true;
}
return false;
} catch (_) {
return false;
}
}
static String encode(String url) {
final raw = _convert(utf8.encode(url), 8, 5, true);
return const Bech32Codec().encode(Bech32('lnurl', raw), 999);
return const Bech32Codec().encode(Bech32("lnurl", raw), 999);
}
static Uri decode(String encodedUrl) {
@@ -114,9 +192,10 @@ class LNURL {
if (_LUD17_PREFIXES.contains(decodedUri.scheme)) {
/// If the non-bech32 LNURL is a Tor address, the port has to be http instead of https for the clearnet LNURL so check if the host ends with '.onion' or '.onion.'
decodedUri = decodedUri.replace(
scheme: decodedUri.host.endsWith('onion') || decodedUri.host.endsWith('onion.')
? 'http'
: 'https');
scheme: decodedUri.host.endsWith("onion") || decodedUri.host.endsWith("onion.")
? "http"
: "https",
);
} else {
/// Try to parse the input as a lnUrl. Will throw an error if it fails.
final lnUrl = _findLnUrl(encodedUrl);
@@ -129,12 +208,30 @@ class LNURL {
}
}
class LNURLWithdrawRequest {
const LNURLWithdrawRequest({
required this.callback,
required this.k1,
required this.description,
required this.minAmount,
required this.maxAmount,
});
final String callback;
final String k1;
final String description;
final Money minAmount;
final Money maxAmount;
}
/// Parse and return a given lnurl string if it's valid. Will remove
/// `lightning:` from the beginning of it if present.
String _findLnUrl(String input) {
final res = RegExp(r',*?((lnurl)([0-9]+[a-z0-9]+))').allMatches(input.toLowerCase());
final res = RegExp(r",*?((lnurl)([0-9]+[a-z0-9]+))").allMatches(input.toLowerCase());
if (res.length != 1) throw ArgumentError('Not a valid lnurl string');
if (res.length != 1) {
throw ArgumentError("Not a valid lnurl string");
}
return res.first.group(0)!;
}
@@ -156,10 +253,16 @@ List<int> _convert(List<int> data, int inBits, int outBits, bool pad) {
}
if (pad) {
if (bits > 0) result.add((value << (outBits - bits)) & maxV);
if (bits > 0) {
result.add((value << (outBits - bits)) & maxV);
}
} else {
if (bits >= inBits) throw Exception('[BECH32] Excess padding');
if ((value << (outBits - bits)) & maxV > 0) throw Exception('[BECH32] Non-zero padding');
if (bits >= inBits) {
throw Exception("[BECH32] Excess padding");
}
if ((value << (outBits - bits)) & maxV > 0) {
throw Exception("[BECH32] Non-zero padding");
}
}
return result;
+119 -181
View File
@@ -1,4 +1,4 @@
import 'package:cw_core/format_fixed.dart';
import "package:cw_core/format_fixed.dart";
abstract class PaymentURI {
PaymentURI({required this.amount, required this.address});
@@ -19,7 +19,7 @@ class MoneroURI extends PaymentURI {
@override
String toString() {
var base = 'monero:$address';
var base = "monero:$address";
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -34,7 +34,7 @@ class HavenURI extends PaymentURI {
@override
String toString() {
var base = 'haven:$address';
var base = "haven:$address";
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -45,7 +45,7 @@ class HavenURI extends PaymentURI {
}
class BitcoinURI extends PaymentURI {
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
BitcoinURI({required super.amount, required super.address, this.pjUri = ""});
final String pjUri;
@@ -53,13 +53,16 @@ class BitcoinURI extends PaymentURI {
String toString() {
final qp = <String, String>{};
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
if (amount.isNotEmpty) {
qp["amount"] = amount.replaceAll(",", ".");
}
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
if (pjUri.isNotEmpty && !address.startsWith("sp")) {
qp["pjos"] = "0";
qp["pj"] = pjUri;
}
return Uri(scheme: "bitcoin", path: address, queryParameters: qp).toString();
}
}
@@ -68,7 +71,7 @@ class LitecoinURI extends PaymentURI {
@override
String toString() {
var base = 'litecoin:$address';
var base = "litecoin:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -83,52 +86,7 @@ class EthereumURI extends PaymentURI {
@override
String toString() {
var base = 'ethereum:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BaseURI extends PaymentURI {
BaseURI({required super.amount, required super.address});
@override
String toString() {
var base = 'base:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class ArbitrumURI extends PaymentURI {
ArbitrumURI({required super.amount, required super.address});
@override
String toString() {
var base = 'arbitrum:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class BSCURI extends PaymentURI {
BSCURI({required super.amount, required super.address});
@override
String toString() {
var base = 'bsc:$address';
var base = "ethereum:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -158,22 +116,7 @@ class NanoURI extends PaymentURI {
@override
String toString() {
var base = 'nano:$address';
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
return base;
}
}
class PolygonURI extends PaymentURI {
PolygonURI({required super.amount, required super.address});
@override
String toString() {
var base = 'polygon:$address';
var base = "nano:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
}
@@ -189,14 +132,14 @@ class SolanaURI extends PaymentURI {
@override
String toString() {
var base = 'solana:$address';
var base = "solana:$address";
final params = <String>[];
if (amount.isNotEmpty) {
params.add('amount=${amount.replaceAll(',', '.')}');
}
if (contractAddress != null && contractAddress!.isNotEmpty) {
params.add('spl-token=$contractAddress');
params.add("spl-token=$contractAddress");
}
if (params.isNotEmpty) {
base += '?${params.join('&')}';
@@ -213,14 +156,14 @@ class TronURI extends PaymentURI {
@override
String toString() {
var base = 'tron:$address';
var base = "tron:$address";
final params = <String>[];
if (amount.isNotEmpty) {
params.add('amount=${amount.replaceAll(',', '.')}');
}
if (contractAddress != null && contractAddress!.isNotEmpty) {
params.add('token=$contractAddress');
params.add("token=$contractAddress");
}
if (params.isNotEmpty) {
base += '?${params.join('&')}';
@@ -235,7 +178,7 @@ class WowneroURI extends PaymentURI {
@override
String toString() {
var base = 'wownero:$address';
var base = "wownero:$address";
if (amount.isNotEmpty) {
base += '?tx_amount=${amount.replaceAll(',', '.')}';
@@ -246,12 +189,11 @@ class WowneroURI extends PaymentURI {
}
class ZanoURI extends PaymentURI {
ZanoURI({required String amount, required String address})
: super(amount: amount, address: address);
ZanoURI({required super.amount, required super.address});
@override
String toString() {
var base = 'zano:' + address;
var base = "zano:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -262,12 +204,11 @@ class ZanoURI extends PaymentURI {
}
class DecredURI extends PaymentURI {
DecredURI({required String amount, required String address})
: super(amount: amount, address: address);
DecredURI({required super.amount, required super.address});
@override
String toString() {
var base = 'decred:' + address;
var base = "decred:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -278,12 +219,11 @@ class DecredURI extends PaymentURI {
}
class DogeURI extends PaymentURI {
DogeURI({required String amount, required String address})
: super(amount: amount, address: address);
DogeURI({required super.amount, required super.address});
@override
String toString() {
var base = 'doge:' + address;
var base = "doge:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -298,7 +238,7 @@ class ZcashURI extends PaymentURI {
@override
String toString() {
var base = 'zcash:$address';
var base = "zcash:$address";
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
@@ -309,9 +249,6 @@ class ZcashURI extends PaymentURI {
}
class ERC681URI extends PaymentURI {
final int chainId;
final String? contractAddress;
ERC681URI({
required this.chainId,
required super.address,
@@ -319,74 +256,11 @@ class ERC681URI extends PaymentURI {
required this.contractAddress,
});
@override
String toString() {
var uri = 'ethereum:';
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
uri += '@$chainId';
}
if (contractAddress != null) {
uri += '/transfer';
}
final params = <String, String>{};
if (contractAddress != null) {
params['address'] = address;
if (amount.isNotEmpty) {
params['uint256'] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
params['value'] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
uri += '?';
uri += params.entries.map((e) => '${e.key}=${e.value}').join('&');
}
return uri;
}
/// Formats amount for ERC-20 token transfers (in atomic units)
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
final amountDouble = double.parse(amount.replaceAll(',', '.'));
final amountBigInt = BigInt.from(amountDouble * 1e18);
return amountBigInt.toString();
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(',', '.');
}
}
/// Formats amount for native ETH payments (in wei using scientific notation)
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
final amountDouble = double.parse(amount.replaceAll(',', '.'));
// Use scientific notation as recommended by ERC-681
return '${amountDouble}e18';
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(',', '.');
}
}
factory ERC681URI.fromUri(Uri uri) {
final (isContract, targetAddress) = _getTargetAddress(uri.path);
final chainId = _getChainID(uri.path);
final address = isContract ? uri.queryParameters["address"] ?? '' : targetAddress;
final address = isContract ? uri.queryParameters["address"] ?? "" : targetAddress;
final amountParam = isContract ? uri.queryParameters["uint256"] : uri.queryParameters["value"];
var formatedAmount = "";
@@ -406,16 +280,78 @@ class ERC681URI extends PaymentURI {
);
}
static int _getChainID(String path) {
return int.parse(RegExp(
r'@\d*',
).firstMatch(path)?.group(0)?.replaceAll("@", "") ??
"1");
final int chainId;
final String? contractAddress;
@override
String toString() {
var uri = "ethereum:";
final targetAddress = contractAddress ?? address;
uri += targetAddress;
if (chainId != 1) {
uri += "@$chainId";
}
if (contractAddress != null) {
uri += "/transfer";
}
final params = <String, String>{};
if (contractAddress != null) {
params["address"] = address;
if (amount.isNotEmpty) {
params["uint256"] = _formatAmountForERC20(amount);
}
} else {
if (amount.isNotEmpty) {
params["value"] = _formatAmountForNative(amount);
}
}
if (params.isNotEmpty) {
uri += "?${params.entries.map((e) => "${e.key}=${e.value}").join("&")}";
}
return uri;
}
/// Formats amount for ERC-20 token transfers (in atomic units)
String _formatAmountForERC20(String amount) {
try {
// Convert decimal amount to BigInt (assuming 18 decimals)
final amountDouble = double.parse(amount.replaceAll(",", "."));
final amountBigInt = BigInt.from(amountDouble * 1e18);
return amountBigInt.toString();
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(",", ".");
}
}
/// Formats amount for native ETH payments (in wei using scientific notation)
String _formatAmountForNative(String amount) {
try {
// Convert decimal amount to double for scientific notation
final amountDouble = double.parse(amount.replaceAll(",", "."));
// Use scientific notation as recommended by ERC-681
return "${amountDouble}e18";
} catch (e) {
// Fallback to original amount if parsing fails
return amount.replaceAll(",", ".");
}
}
static int _getChainID(String path) => int.parse(
RegExp(r"@\d*").firstMatch(path)?.group(0)?.replaceAll("@", "") ?? "1",
);
static (bool, String) _getTargetAddress(String path) {
final targetAddress =
RegExp(r'^(0x)?[0-9a-f]{40}', caseSensitive: false).firstMatch(path)!.group(0)!;
RegExp(r"^(0x)?[0-9a-f]{40}", caseSensitive: false).firstMatch(path)!.group(0)!;
return (path.contains("/"), targetAddress);
}
@@ -426,25 +362,27 @@ class ERC681URI extends PaymentURI {
/// - Scientific notation: "0.123e18", "1e6" → expanded to integer
/// - Decimal ETH: "0.123456" → shifted by 18 decimals
static String _normalizeToIntegerWei(String input) {
final raw = input.replaceAll(',', '.').trim();
final raw = input.replaceAll(",", ".").trim();
// First we check if it's already a plain integer (basically just a number with no dot, no exponent)
try {
final isPlainInteger = RegExp(r'^[+-]?\d+$').hasMatch(raw) &&
!raw.contains('.') &&
!raw.toLowerCase().contains('e');
if (isPlainInteger) return raw.replaceFirst(RegExp(r'^\+'), '');
final isPlainInteger = RegExp(r"^[+-]?\d+$").hasMatch(raw) &&
!raw.contains(".") &&
!raw.toLowerCase().contains("e");
if (isPlainInteger) {
return raw.replaceFirst(RegExp(r"^\+"), "");
}
// Then we check if it's a scientific notation
final sci = RegExp(r'^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$');
final sci = RegExp(r"^[+-]?(\d+\.?\d*|\d*\.?\d+)[eE][+-]?\d+$");
if (sci.hasMatch(raw)) {
final mantissaStr = raw.toLowerCase().split('e')[0];
final exp = int.parse(raw.toLowerCase().split('e')[1]);
final mantissaStr = raw.toLowerCase().split("e")[0];
final exp = int.parse(raw.toLowerCase().split("e")[1]);
return _expandDecimal(mantissaStr, exp);
}
// Lastly, we check if it's a fixed decimal ETH amount, here we shift by 18 to get wei for the amount
if (raw.contains('.')) {
if (raw.contains(".")) {
return _expandDecimal(raw, 18);
}
return raw;
@@ -462,34 +400,34 @@ class ERC681URI extends PaymentURI {
/// _expandDecimal('1.2', 3) -> '1200'
static String _expandDecimal(String decimalStr, int expShift) {
var s = decimalStr.trim();
var sign = '';
if (s.startsWith('-') || s.startsWith('+')) {
sign = s[0] == '-' ? '-' : '';
var sign = "";
if (s.startsWith("-") || s.startsWith("+")) {
sign = s[0] == "-" ? "-" : "";
s = s.substring(1);
}
// First we split the integer and fractional parts
final parts = s.split('.');
final intPart = parts[0].isEmpty ? '0' : parts[0];
final fracPart = parts.length > 1 ? parts[1] : '';
final digits = (intPart + fracPart).replaceFirst(RegExp(r'^0+'), '');
final parts = s.split(".");
final intPart = parts[0].isEmpty ? "0" : parts[0];
final fracPart = parts.length > 1 ? parts[1] : "";
final digits = (intPart + fracPart).replaceFirst(RegExp(r"^0+"), "");
final fracLen = fracPart.length;
// Then we calculate the effective shift = desired shift minus existing fractional digits
final shift = expShift - fracLen;
if (shift >= 0) {
final head = digits.isEmpty ? '0' : digits;
final zeros = List.filled(shift, '0').join();
final head = digits.isEmpty ? "0" : digits;
final zeros = List.filled(shift, "0").join();
final res = head + zeros;
return sign + (res.isEmpty ? '0' : res);
return sign + (res.isEmpty ? "0" : res);
} else {
// Need to insert a decimal point within digits; return integer by truncating
final cut = digits.length + shift;
if (cut <= 0) {
return '0';
return "0";
}
final res = digits.substring(0, cut);
return sign + (res.isEmpty ? '0' : res);
return sign + (res.isEmpty ? "0" : res);
}
}
}
+5 -2
View File
@@ -586,11 +586,12 @@ class WalletInfo {
"addressPageType": addressPageType,
"receiveInfoboxDismissed": receiveInfoboxDismissed ? 1 : 0,
"showCombinedBalance": showCombinedBalance ? 1 : 0,
"favoriteTokenAddress": favoriteTokenAddress
"favoriteTokenAddress": favoriteTokenAddress,
"network": network,
};
factory WalletInfo.fromJson(Map<String, dynamic> json) {
return WalletInfo(
final info = WalletInfo(
json[selfIdColumn] as int,
json['id'] as String,
json['name'] as String,
@@ -616,6 +617,8 @@ class WalletInfo {
json['receiveInfoboxDismissed'] != 0,
json["showCombinedBalance"] != 0,
json["favoriteTokenAddress"] as String? ?? null);
info.network = json['network'] as String?;
return info;
}
Future<int> save() async {
+2 -1
View File
@@ -274,7 +274,8 @@ class WalletInfo extends HiveObject {
addressPageType,
false,
true,
null);
null)
..network = network;
final wiId = await walletInfo.save();
for (final address in usedAddresses ?? <String>[]) {
await newWi.WalletInfoAddress.insert(wiId, newWi.WalletInfoAddressType.used, address);
+5 -5
View File
@@ -394,10 +394,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -738,10 +738,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: "direct main"
description:
@@ -838,5 +838,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.27.0"
+128
View File
@@ -0,0 +1,128 @@
import "package:cw_core/amount/money.dart";
import "package:cw_core/amount/money_local.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter_test/flutter_test.dart";
import "package:intl/intl.dart";
final _btc012 = Money.fromInt(12345678, CryptoCurrency.btc); // 0.12345678 BTC
final _btcTrailing = Money.fromInt(12000000, CryptoCurrency.btc); // 0.12 BTC
final _btcGrouping = Money.fromInt(123450000000, CryptoCurrency.btc); // 1234.5 BTC
final _xmrHalf = Money.fromInt(500000000000, CryptoCurrency.xmr); // 0.5 XMR
void main() {
group("toLocalStringWithPrecision", () {
test("localizes grouping (en_US)", () {
expect(_btcGrouping.toLocalStringWithPrecision(locale: "en_US"), "1,234.5");
});
test("no grouping for sub-1 values", () {
expect(_btc012.toLocalStringWithPrecision(locale: "en_US"), "0.12345678");
});
test("swaps grouping/decimal separators (de_DE)", () {
expect(_btcGrouping.toLocalStringWithPrecision(locale: "de_DE"), "1.234,5");
});
test("base unit renders integer sats with grouping", () {
expect(
_btc012.toLocalStringWithPrecision(useBaseUnit: true, locale: "en_US"),
"12,345,678",
);
});
test("fractionalDigits truncates (does not round)", () {
// 0.12345678 -> 0.12 (NOT 0.13).
expect(_btc012.toLocalStringWithPrecision(fractionalDigits: 2, locale: "en_US"), "0.12");
});
test("trimZeros:false keeps trailing zeros", () {
expect(
_btcTrailing.toLocalStringWithPrecision(trimZeros: false, locale: "en_US"),
"0.12000000",
);
});
test("respects a higher-precision currency (XMR)", () {
expect(_xmrHalf.toLocalStringWithPrecision(locale: "en_US"), "0.5");
});
test("falls back to the ambient Intl locale when locale is null", () {
final previous = Intl.defaultLocale;
addTearDown(() => Intl.defaultLocale = previous);
Intl.defaultLocale = "de_DE";
// Proves the extension passes null through to NumberFormat rather than
// hardcoding a locale.
expect(_btcGrouping.toLocalStringWithPrecision(), "1.234,5");
});
});
group("toLocalStringWithSymbol", () {
test("suffixes the symbol by default", () {
expect(_btcGrouping.toLocalStringWithSymbol(locale: "en_US"), "1,234.5 BTC");
});
test("prefixes the symbol when withSymbolPrefix is true", () {
expect(
_btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "en_US"),
"BTC 1,234.5",
);
});
test("suffixes the base-unit ticker", () {
expect(
_btc012.toLocalStringWithSymbol(useBaseUnit: true, locale: "en_US"),
"12,345,678 sats",
);
});
test("prefixes the base-unit ticker", () {
expect(
_btc012.toLocalStringWithSymbol(
useBaseUnit: true,
withSymbolPrefix: true,
locale: "en_US",
),
"sats 12,345,678",
);
});
test("prefix respects de_DE separators", () {
expect(
_btcGrouping.toLocalStringWithSymbol(withSymbolPrefix: true, locale: "de_DE"),
"BTC 1.234,5",
);
});
test("uses the currency symbol for non-BTC", () {
expect(_xmrHalf.toLocalStringWithSymbol(locale: "en_US"), "0.5 XMR");
});
test("forwards fractionalDigits to the precision string", () {
expect(_btc012.toLocalStringWithSymbol(fractionalDigits: 2, locale: "en_US"), "0.12 BTC");
});
test("forwards trimZeros to the precision string", () {
expect(
_btcTrailing.toLocalStringWithSymbol(trimZeros: false, locale: "en_US"),
"0.12000000 BTC",
);
});
});
group("negative amounts", () {
test("keeps the sign when the integer part is non-zero", () {
expect((-_btcGrouping).toLocalStringWithSymbol(locale: "en_US"), "-1,234.5 BTC");
});
// NOTE: currently fails. For |amount| < 1 the sign is dropped:
// formatFixed yields "-0.5", but _withLocalSeparator re-parses the integer
// part and int.tryParse("-0") == 0, so the "-" is lost -> "0.5 XMR".
// Fix in _withLocalSeparator by capturing the sign before parsing, e.g.
// final negative = amount.startsWith("-");
// ... then re-apply it to the formatted result.
test("keeps the sign for sub-1 amounts", () {
expect((-_xmrHalf).toLocalStringWithSymbol(locale: "en_US"), "-0.5 XMR");
});
});
}
+30 -4
View File
@@ -278,10 +278,14 @@ void main() {
});
test("handles exact division perfectly", () {
expect((Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(5));
expect((Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(-5));
expect(
(Money(BigInt.from(100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(5),
);
expect(
(Money(BigInt.from(-100), CryptoCurrency.btc) / BigInt.from(20)).amount,
BigInt.from(-5),
);
});
});
});
@@ -346,6 +350,28 @@ void main() {
expect(money.currency.decimals, equals(8));
expect(money.toStringWithSymbol(useBaseUnit: true), "100000000 sats");
});
group("withSymbolPrefix", () {
test("with fractionalDigits and padded with zeros", () {
final money = Money.parse("1", CryptoCurrency.btc);
expect(money.amount, equals(BigInt.parse("100000000")));
expect(money.currency.decimals, equals(8));
expect(
money.toStringWithSymbol(fractionalDigits: 5, trimZeros: false, withSymbolPrefix: true),
"BTC 1.00000",
);
});
test("using base unit sats", () {
final money = Money.parse("1", CryptoCurrency.btc);
expect(money.amount, equals(BigInt.parse("100000000")));
expect(money.currency.decimals, equals(8));
expect(
money.toStringWithSymbol(useBaseUnit: true, withSymbolPrefix: true),
"sats 100000000",
);
});
});
});
});
}
+4
View File
@@ -1,6 +1,8 @@
import "package:analysis_server_plugin/plugin.dart";
import "package:analysis_server_plugin/registry.dart";
import "package:cw_custom_lints/http_force_proxy/http_force_proxy_rule.dart";
import "package:cw_custom_lints/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart";
import "package:cw_custom_lints/modern_button_semantics/modern_button_semantics_rule.dart";
import "package:cw_custom_lints/print_verbose/print_verbose_fix.dart";
import "package:cw_custom_lints/print_verbose/print_verbose_rule.dart";
import "package:cw_custom_lints/restricted_imports/restricted_imports_rule.dart";
@@ -16,6 +18,8 @@ class CwCustomLintsPlugin extends Plugin {
registry.registerWarningRule(PrintVerboseRule());
registry.registerWarningRule(RestrictedImportsRule());
registry.registerWarningRule(HttpForceProxyRule());
registry.registerWarningRule(ModernButtonSemanticsRule());
registry.registerWarningRule(ModalTopBarSemanticsRule());
registry.registerFixForRule(PrintVerboseRule.code, ReplaceWithPrintV.new);
}
@@ -0,0 +1,69 @@
import "package:analyzer/analysis_rule/analysis_rule.dart";
import "package:analyzer/analysis_rule/rule_context.dart";
import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
import "package:analyzer/dart/ast/ast.dart";
import "package:analyzer/dart/ast/visitor.dart";
import "package:analyzer/error/error.dart";
import "package:cw_custom_lints/utils/widget_arguments.dart";
class ModalTopBarSemanticsRule extends AnalysisRule {
ModalTopBarSemanticsRule()
: super(
name: "require_modal_top_bar_semantics",
description:
"ModalTopBar icons need a semantic label, because the icon alone does not say whether it closes the modal, goes back or does something else.",
);
static const LintCode code = LintCode(
"require_modal_top_bar_semantics",
"ModalTopBar was given a {0} without a {1}, so screen readers have nothing to announce.",
correctionMessage: "Pass a localized {1} next to the {0}.",
severity: DiagnosticSeverity.WARNING,
);
static const _iconSemanticLabels = {
"leadingIcon": "leadingSemanticLabel",
"trailingIcon": "trailingSemanticLabel",
};
@override
LintCode get diagnosticCode => code;
@override
void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) =>
registry.addInstanceCreationExpression(this, _Visitor(this));
}
class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule);
final AnalysisRule rule;
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
if (!isCakeWalletWidget(node.constructorName, "ModalTopBar")) {
return;
}
for (final slot in ModalTopBarSemanticsRule._iconSemanticLabels.entries) {
_checkSlot(node.argumentList, iconName: slot.key, semanticLabelName: slot.value);
}
}
void _checkSlot(
ArgumentList argumentList, {
required String iconName,
required String semanticLabelName,
}) {
final icon = namedArgument(argumentList, iconName);
if (icon == null || icon.argumentExpression is NullLiteral) {
return;
}
if (!isMissingOrEmptyText(namedArgumentExpression(argumentList, semanticLabelName))) {
return;
}
rule.reportAtToken(icon.name, arguments: [iconName, semanticLabelName]);
}
}
@@ -0,0 +1,56 @@
import "package:analyzer/analysis_rule/analysis_rule.dart";
import "package:analyzer/analysis_rule/rule_context.dart";
import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
import "package:analyzer/dart/ast/ast.dart";
import "package:analyzer/dart/ast/visitor.dart";
import "package:analyzer/error/error.dart";
import "package:cw_custom_lints/utils/widget_arguments.dart";
class ModernButtonSemanticsRule extends AnalysisRule {
ModernButtonSemanticsRule()
: super(
name: "require_modern_button_semantics",
description:
"ModernButton needs a semanticLabel when it has no visible label, otherwise screen readers announce an unnamed button.",
);
static const LintCode code = LintCode(
"require_modern_button_semantics",
"ModernButton has no visible label, so screen readers have nothing to announce.",
correctionMessage: "Pass a localized semanticLabel, or a non-empty label.",
severity: DiagnosticSeverity.WARNING,
);
@override
LintCode get diagnosticCode => code;
@override
void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) =>
registry.addInstanceCreationExpression(this, _Visitor(this));
}
class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule);
final AnalysisRule rule;
@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
final constructorName = node.constructorName;
if (!isCakeWalletWidget(constructorName, "ModernButton")) {
return;
}
final argumentList = node.argumentList;
final hasLabel = !isMissingOrEmptyText(namedArgumentExpression(argumentList, "label"));
final hasSemanticLabel = !isMissingOrEmptyText(
namedArgumentExpression(argumentList, "semanticLabel"),
);
if (hasLabel || hasSemanticLabel) {
return;
}
rule.reportAtNode(constructorName);
}
}
@@ -0,0 +1,51 @@
import "package:analyzer/analysis_rule/analysis_rule.dart";
import "package:analyzer/analysis_rule/rule_context.dart";
import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
import "package:analyzer/dart/ast/ast.dart";
import "package:analyzer/dart/ast/visitor.dart";
import "package:analyzer/error/error.dart";
class NoBareCatchRule extends AnalysisRule {
NoBareCatchRule()
: super(
name: "no_bare_catch",
description: "please specify the exception class to catch (on SomeException catch (e))",
);
static const LintCode code = LintCode(
"no_bare_catch",
"please specify the exception class to catch (on SomeException catch (e))",
severity: DiagnosticSeverity.WARNING,
);
@override
LintCode get diagnosticCode => code;
@override
void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) {
registry.addCatchClause(this, _Visitor(this));
}
}
class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule);
final AnalysisRule rule;
@override
void visitCatchClause(CatchClause node) {
if (node.exceptionType != null) {
return;
}
final tryStatement = node.parent;
if (tryStatement is TryStatement) {
final precedingClauses = tryStatement.catchClauses.takeWhile((clause) => clause != node);
if (precedingClauses.any((clause) => clause.exceptionType != null)) {
return;
}
}
rule.reportAtNode(node);
}
}
@@ -0,0 +1,39 @@
import "package:analyzer/dart/ast/ast.dart";
bool isCakeWalletWidget(ConstructorName constructorName, String className) {
final element = constructorName.type.element;
if (element == null || element.name != className) {
return false;
}
final libraryUri = element.library?.uri;
if (libraryUri == null || libraryUri.scheme != "package") {
return false;
}
final segments = libraryUri.pathSegments;
return segments.isNotEmpty && segments.first == "cake_wallet";
}
NamedArgument? namedArgument(ArgumentList argumentList, String name) {
for (final argument in argumentList.arguments) {
if (argument is NamedArgument && argument.name.lexeme == name) {
return argument;
}
}
return null;
}
Expression? namedArgumentExpression(ArgumentList argumentList, String name) =>
namedArgument(argumentList, name)?.argumentExpression;
// passed null or ""
// does NOT work if a runtime expression evaluates to an empty string or null
bool isMissingOrEmptyText(Expression? expression) {
if (expression == null || expression is NullLiteral) {
return true;
}
return expression is StringLiteral && (expression.stringValue?.isEmpty ?? false);
}
+429
View File
@@ -0,0 +1,429 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev"
source: hosted
version: "100.0.0"
analysis_server_plugin:
dependency: "direct main"
description:
name: analysis_server_plugin
sha256: c4021359e7a673ecd9c6db5f11fea12fabb55e0ba96fa08c78920ae81936f1d1
url: "https://pub.dev"
source: hosted
version: "0.3.15"
analyzer:
dependency: "direct main"
description:
name: analyzer
sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev"
source: hosted
version: "13.0.0"
analyzer_plugin:
dependency: "direct main"
description:
name: analyzer_plugin
sha256: "9a81dc36f7f71890d7cf83960a3a7df1e6c9a24035f98eb848e91ad48b1d21c6"
url: "https://pub.dev"
source: hosted
version: "0.14.9"
analyzer_testing:
dependency: "direct dev"
description:
name: analyzer_testing
sha256: da27ae9bd4bf3ce4ebab6295cf922ca21a2b14ea8fcacc859342aca796a48a2d
url: "https://pub.dev"
source: hosted
version: "0.2.6"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev"
source: hosted
version: "1.15.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
url: "https://pub.dev"
source: hosted
version: "3.1.9"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
meta:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
url: "https://pub.dev"
source: hosted
version: "1.31.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
url: "https://pub.dev"
source: hosted
version: "0.7.13"
test_core:
dependency: transitive
description:
name: test_core
sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
url: "https://pub.dev"
source: hosted
version: "0.6.19"
test_reflective_loader:
dependency: "direct dev"
description:
name: test_reflective_loader
sha256: d828d5ca15179aaac2aaf8f510cf0a52ec28e0031681b044ec5e581a4b8002e7
url: "https://pub.dev"
source: hosted
version: "0.4.0"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yaml_edit:
dependency: transitive
description:
name: yaml_edit
sha256: "07c9e63ba42519745182b88ca12264a7ba2484d8239958778dfe4d44fe760488"
url: "https://pub.dev"
source: hosted
version: "2.2.4"
sdks:
dart: ">=3.11.0 <4.0.0"
+4 -4
View File
@@ -417,10 +417,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -769,10 +769,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+13 -26
View File
@@ -1,18 +1,15 @@
import 'dart:developer';
import "dart:developer";
import 'package:cw_core/payment_uris.dart';
import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:mobx/mobx.dart';
import "package:cw_core/payment_uris.dart";
import "package:cw_core/wallet_addresses.dart";
import "package:mobx/mobx.dart";
part 'evm_chain_wallet_addresses.g.dart';
part "evm_chain_wallet_addresses.g.dart";
class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses;
abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
EVMChainWalletAddressesBase(WalletInfo walletInfo, this._selectedChainId)
: address = '',
super(walletInfo);
EVMChainWalletAddressesBase(super.walletInfo, this._selectedChainId) : address = "";
@override
@observable
@@ -33,7 +30,7 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
Future<void> updateAddressesInBox() async {
try {
addressesMap.clear();
addressesMap[address] = '';
addressesMap[address] = "";
await saveAddressesInBox();
} catch (e) {
log(e.toString());
@@ -41,20 +38,10 @@ abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store {
}
@override
PaymentURI getPaymentUri(String amount) {
switch (_selectedChainId) {
case 1:
return EthereumURI(amount: amount, address: address);
case 137:
return PolygonURI(amount: amount, address: address);
case 8453:
return BaseURI(amount: amount, address: address);
case 42161:
return ArbitrumURI(amount: amount, address: address);
case 56:
return BSCURI(amount: amount, address: address);
default:
return EthereumURI(amount: amount, address: address);
}
}
PaymentURI getPaymentUri(String amount) => ERC681URI(
address: address,
amount: amount,
chainId: _selectedChainId,
contractAddress: null,
);
}
+4
View File
@@ -257,6 +257,10 @@ void setRecoveringFromSeed({required bool isRecovery}) {
final storeMutex = Mutex();
void refresh() {
currentWallet!.refresh();
}
int lastStorePointer = 0;
int lastStoreHeight = 0;
void storeSync({bool force = false}) async {
+5
View File
@@ -150,6 +150,11 @@ void restoreWalletFromSeedSync(
_lastOpenedWallet = path;
}
void storePassphrase({required String path, required String passphrase}) {
currentWallet!.setCacheAttribute(key: "cakewallet.passphrase", value: passphrase);
currentWallet!.store(path: path);
}
void restoreWalletFromKeys(
{required String path,
required String password,
+15 -5
View File
@@ -370,6 +370,8 @@ abstract class MoneroWalletBase
};
}
bool hasUnknownKeyImages() => currentWallet!.hasUnknownKeyImages();
bool needExportOutputs(Money amount) {
if (int.tryParse(currentWallet!.secretSpendKey()) != 0) {
return false;
@@ -382,22 +384,30 @@ abstract class MoneroWalletBase
}
MoneroTrezorService? trezorService;
Future<Trezor> _getTrezor() async {
if (trezorService == null) throw Exception("Trezor not connected");
final trezor = Trezor(trezorService!);
await trezor.newPassphraseSession(passphrase);
return trezor;
}
Future<void> syncTrezor() async {
if (trezorService == null) throw Exception("Trezor not connected");
final ptr = Pointer<Void>.fromAddress(currentWallet!.ffiAddress());
final tdis = monero.Wallet_exportTrezorTdis(ptr);
final response = await Trezor(trezorService!).keyImageSync(tdis);
final trezor = await _getTrezor();
final response = await trezor.keyImageSync(tdis);
final success = monero.Wallet_importTrezorEncryptedKeyImagesJson(ptr, response);
if (!success) throw Exception(monero.Wallet_errorString(ptr));
}
Future<String> signTrezorTransaction(String json) async {
if (trezorService == null) throw Exception("Trezor not connected");
return Trezor(trezorService!).signTransaction(json);
final trezor = await _getTrezor();
return trezor.signTransaction(json);
}
@override
+2 -2
View File
@@ -90,7 +90,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
addressesMap.clear();
addressInfos.clear();
accountList.accounts.forEach((account) async {
for (final account in accountList.accounts) {
await _subaddressList.update(accountIndex: account.id);
_subaddressList.subaddresses.forEach((subaddress) {
addressesMap[subaddress.address] = subaddress.label;
@@ -102,7 +102,7 @@ abstract class MoneroWalletAddressesBase extends WalletAddresses with Store {
address: subaddress.address,
label: subaddress.label));
});
});
}
await saveAddressesInBox();
} catch (e) {
+20 -5
View File
@@ -44,10 +44,15 @@ class MoneroNewWalletCredentials extends WalletCredentials {
}
class MoneroRestoreWalletFromHardwareCredentials extends WalletCredentials {
MoneroRestoreWalletFromHardwareCredentials(
{required String name, required this.hardwareWalletService, int height = 0, String? password})
: super(name: name, password: password, height: height);
HardwareWalletService hardwareWalletService;
MoneroRestoreWalletFromHardwareCredentials({
required String name,
required this.hardwareWalletService,
int height = 0,
String? password,
String? passphrase,
}) : super(name: name, password: password, height: height, passphrase: passphrase);
final HardwareWalletService hardwareWalletService;
}
class MoneroRestoreWalletFromSeedCredentials extends WalletCredentials {
@@ -306,7 +311,13 @@ class MoneroWalletService extends WalletService<
deviceName: 'Ledger',
);
} else if (credentials.hardwareWalletService case MoneroTrezorService service) {
final watchCredentials = await Trezor(service).getWatchCredentials();
final trezor = Trezor(service);
if (credentials.passphrase != null) {
await trezor.newPassphraseSession(credentials.passphrase);
}
final watchCredentials = await trezor.getWatchCredentials();
monero_wallet_manager.restoreWalletFromKeys(
path: path,
@@ -317,6 +328,10 @@ class MoneroWalletService extends WalletService<
viewKey: watchCredentials.watchKey,
spendKey: "",
);
if (credentials.passphrase != null) {
monero_wallet_manager.storePassphrase(path: path, passphrase: credentials.passphrase!);
}
}
final wallet = MoneroWallet(
+30 -18
View File
@@ -1,25 +1,35 @@
import 'dart:convert';
import "dart:convert";
import 'package:cw_core/hardware/hardware_wallet_service.dart';
import 'package:trezor_flutter/trezor_flutter.dart';
import "package:cw_core/hardware/hardware_wallet_service.dart";
import "package:trezor_flutter/trezor_flutter.dart";
class MoneroTrezorService extends HardwareWalletService {
final TrezorClient client;
MoneroTrezorService(this.client);
final TrezorClient client;
}
class MoneroTrezorWatchCredentials {
const MoneroTrezorWatchCredentials(this.watchKey, this.address);
final String watchKey;
final String address;
const MoneroTrezorWatchCredentials(this.watchKey, this.address);
}
class Trezor {
Trezor(this.service);
final MoneroTrezorService service;
Trezor(this.service);
String? _sessionPassphrase;
Future<void> newPassphraseSession(String? passphrase) async {
if (passphrase == null) return;
if (_sessionPassphrase == passphrase) return;
_sessionPassphrase = passphrase;
return service.client.createChannel(passphrase: _sessionPassphrase);
}
Future<MoneroTrezorWatchCredentials> getWatchCredentials() async {
final credentials = await TrezorMonero(service.client).getWatchCredentials();
@@ -33,21 +43,23 @@ class Trezor {
final txIds = <MoneroKeyImageTxData>[];
for (final tdi in tdisList) {
txIds.add(MoneroKeyImageTxData(
outKey: tdi["out_key"],
txPubKey: tdi["tx_pub_key"],
internalOutputIndex: tdi["internal_output_index"],
subAddrMajor: tdi["sub_addr_major"],
subAddrMinor: tdi["sub_addr_minor"],
txIds.add(
MoneroKeyImageTxData(
outKey: tdi["out_key"] as String,
txPubKey: tdi["tx_pub_key"] as String,
internalOutputIndex: tdi["internal_output_index"] as int,
subAddrMajor: tdi["sub_addr_major"] as int,
subAddrMinor: tdi["sub_addr_minor"] as int,
additionalTxPubKeys:
(tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? []));
(tdi["additional_tx_pub_keys"] as List?)?.map((e) => e as String).toList() ?? [],
),
);
}
final keyImages = await TrezorMonero(service.client).syncKeyImages(txIds);
return jsonEncode(keyImages.toMap());
}
Future<String> signTransaction(String json) async {
return TrezorMonero(service.client).signTransaction(jsonDecode(json));
}
Future<String> signTransaction(String json) =>
TrezorMonero(service.client).signTransaction(jsonDecode(json) as Map<String, dynamic>);
}
+9 -9
View File
@@ -505,10 +505,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -906,10 +906,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
@@ -921,8 +921,8 @@ packages:
dependency: "direct main"
description:
path: trezor-flutter
ref: d1289dfa4247a93ee57342dfd249957b339915ef
resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.1.0"
@@ -930,8 +930,8 @@ packages:
dependency: transitive
description:
path: trezor_usb_transport
ref: d1289dfa4247a93ee57342dfd249957b339915ef
resolved-ref: d1289dfa4247a93ee57342dfd249957b339915ef
ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
resolved-ref: "7a736320f6d53ad5c763734ab832d6bea560a327"
url: "https://github.com/cake-tech/trezor-flutter.git"
source: git
version: "0.0.1"
@@ -1048,5 +1048,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.0-0 <4.0.0"
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.24.0"
+1 -1
View File
@@ -33,7 +33,7 @@ dependencies:
trezor_flutter:
git:
url: https://github.com/cake-tech/trezor-flutter.git
ref: d1289dfa4247a93ee57342dfd249957b339915ef
ref: 7d27e19b7bcdcd6241df35953f9a6ec2df0c7a97
path: trezor-flutter
dev_dependencies:
+4 -4
View File
@@ -462,10 +462,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -887,10 +887,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+9
View File
@@ -100,6 +100,15 @@ class DefaultSPLTokens {
iconPath: 'assets/images/avdo_icon.png',
enabled: false,
),
SPLToken(
name: "The Bitcoin Bull",
symbol: "TBB",
mintAddress: "42cXQvAAr7hcPBPWAS4ocVtDyeJ4Fa6gRR2uG4gppump",
decimal: 6,
mint: "tbb",
iconPath: "assets/images/tbb_icon.png",
enabled: false,
),
SPLToken(
name: "Tether Gold",
symbol: "XAUT0",
+44
View File
@@ -52,6 +52,8 @@ class SolanaWalletClient {
SolanaRPC? _provider;
bool _isStopped = false;
final Map<String, bool> _jupiterVerificationCache = {};
bool connect(Node node) {
try {
_isStopped = false;
@@ -1906,6 +1908,48 @@ class SolanaWalletClient {
return [];
}
}
Future<bool?> isTokenVerifiedOnJupiter(String mintAddress) async {
if (_jupiterVerificationCache.containsKey(mintAddress)) {
return _jupiterVerificationCache[mintAddress];
}
try {
final uri = Uri.https(
"lite-api.jup.ag",
"/tokens/v2/search",
{"query": mintAddress},
);
final response = await client.get(
uri,
headers: {"Accept": "application/json"},
);
if (response.statusCode < 200 || response.statusCode >= 300) {
printV("Jupiter token API returned status: ${response.statusCode}");
return null;
}
final decodedResponse = jsonDecode(response.body) as List;
for (final item in decodedResponse) {
final tokenData = item as Map<String, dynamic>;
if (tokenData["id"] == mintAddress) {
final isVerified = tokenData["isVerified"] as bool? ?? false;
_jupiterVerificationCache[mintAddress] = isVerified;
return isVerified;
}
}
// this means Jupiter doesn't index this mint at all, so we can't say either
return null;
} catch (e) {
printV("Error checking Jupiter verification: ${e.toString()}");
return null;
}
}
}
class MoralisSolanaTokenBalance {
+3
View File
@@ -902,6 +902,9 @@ abstract class SolanaWalletBase
}
}
Future<bool?> isTokenVerifiedOnJupiter(String mintAddress) =>
_client.isTokenVerifiedOnJupiter(mintAddress);
void _setTransactionUpdateTimer() {
if (_transactionsUpdateTimer?.isActive ?? false) {
_transactionsUpdateTimer!.cancel();
+4 -4
View File
@@ -417,10 +417,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -786,10 +786,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+4 -4
View File
@@ -422,10 +422,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -783,10 +783,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
version: "0.7.10"
torch_dart:
dependency: transitive
description:
+2 -1
View File
@@ -4,7 +4,6 @@
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';
@@ -12,4 +11,6 @@ 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_network.dart';
export 'src/zcash_receive_page_options.dart';
export 'src/zcash_wallet_addresses.dart';
-196
View File
@@ -1,196 +0,0 @@
import 'dart:io';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:path/path.dart' as p;
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
import 'package:zkool/src/rust/api/key.dart' as zkool_key;
import 'package:zkool/src/rust/api/network.dart' as zkool_network;
import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
import 'package:zkool/src/rust/frb_generated.dart' as zkool_frb;
class ZkoolSweep {
ZkoolSweep({
required this.currentBalance,
required this.cacheDir,
required this.seed,
required this.passphrase,
required this.address,
required this.url,
required this.height,
});
final int currentBalance;
final String seed;
final String cacheDir;
final String passphrase;
final String address;
final String url;
final int height;
static SweepUpdate msg = SweepUpdate(message: 'Idling', currentHeight: 0, networkHeight: 0);
Future<void> start() async {
try {
await _init();
if (!zkool_key.isValidAddress(address: address)) {
msg.message = "$address is not valid";
return;
}
msg = SweepUpdate(message: 'Starting import', currentHeight: 0, networkHeight: 0);
await _newAccount();
msg.message = "Starting sync";
await warpSync();
msg.message = "Waiting";
while (msg.blocksLeft != 0 && msg.networkHeight != 0) {
await Future.delayed(Duration(seconds: 1));
}
await Future.delayed(Duration(seconds: 5));
final balance = await getBalance();
if (balance.toInt() == balance) {
msg.message = "No need to sweep, balance is the same";
return;
}
msg.message = "Sweeping";
await sweep(balance);
} catch (e) {
msg.message = e.toString();
printV("zkool_sweep: $e");
rethrow;
}
}
Future<BigInt> getBalance() async {
final b = await zkool_sync.balance(c: c);
return b.field0.reduce((final a, final b) => a + b);
}
Future<void> sweep(final BigInt amount) async {
msg.message = "prepare";
final tx = await zkool_pay.prepare(
recipients: [zkool_paydart.Recipient(address: address, amount: amount)],
options: zkool_pay.PaymentOptions(
srcPools: 7,
recipientPaysFee: true,
smartTransparent: false,
),
c: c,
);
msg.message = "getLatestHeight";
final height = await getLatestHeight();
msg.message = "signTransaction";
final signTx = await zkool_pay.signTransaction(pczt: tx, c: c);
msg.message = "extractTransaction";
final txBytes = await zkool_pay.extractTransaction(package: signTx);
msg.message = "broadcastTransaction";
final result = await zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: c);
msg.message = "result";
if (result.isEmpty) {
msg.message = "Unknown error";
throw Exception(result);
}
}
static var c = zkool_coin.Coin();
static bool _isInit = false;
File get dbFile => File(p.join(cacheDir, "zkool-tmp-mirror.db"));
Future<int> warpSync() async {
final currentHeight = await getLatestHeight();
msg.networkHeight = currentHeight;
final sync = zkool_sync.synchronize(
accounts: [currentAccount],
currentHeight: currentHeight,
actionsPerSync: 10000,
transparentLimit: 100,
checkpointAge: 200,
c: c,
);
bool success = false;
await sync.listen(
(final syncProgress) {
msg.currentHeight = syncProgress.height;
msg.message = "Sync ${syncProgress.height} / ${syncProgress.time}";
},
onError: (final e) {
msg.message = e.toString();
},
onDone: () {
msg.message = "Sync done!";
success = true;
},
);
if (msg.currentHeight != currentHeight && msg.currentHeight != 0) {
msg.message = "Canceling sync";
await zkool_sync.cancelSync();
await Future.delayed(Duration(seconds: 1));
return warpSync();
}
return 0;
}
Future<int> getLatestHeight() async {
try {
return await zkool_network.getCurrentHeight(c: c);
} catch (e) {
printV("getLatestHeight: $e");
}
return 0;
}
Future<void> _newAccount() async {
final id = await zkool_account.newAccount(
na: zkool_account.NewAccount(
name: 'restoretmp' + DateTime.now().microsecondsSinceEpoch.toString(),
restore: true,
passphrase: "",
key: seed,
aindex: 0,
birth: height,
folder: '',
useInternal: true,
internal: false,
ledger: false,
),
c: c,
);
currentAccount = id;
c = await c.setAccount(account: id);
}
int currentAccount = 0;
Future<void> _init() async {
if (_isInit) return;
_isInit = true;
printV(".init()");
await zkool_frb.RustLib.init();
await zkool_network.initDatadir(directory: dbFile.parent.path);
c = await c.openDatabase(dbFilepath: dbFile.path, password: 'cw_zcash_migration');
c = c.setLwd(url: url, serverType: 0);
printV(".init(): Done");
}
}
class SweepUpdate {
SweepUpdate({required this.message, required this.currentHeight, required this.networkHeight});
String message;
int currentHeight;
int networkHeight;
double get progress {
if (networkHeight == 0) return 0.01;
return currentHeight / networkHeight;
}
int get blocksLeft => networkHeight - currentHeight;
Map<String, dynamic> toJson() => {
"message": message,
"currentHeight": currentHeight,
"networkHeight": networkHeight,
"progress": progress,
};
}
+96
View File
@@ -0,0 +1,96 @@
import 'dart:math';
import 'package:cw_core/get_height_by_date_zec.dart';
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/cw_zcash.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:path/path.dart' as p;
class ZkoolSeed {
ZkoolSeed({required this.seed, required this.passphrase});
factory ZkoolSeed.fromYwalletSeed({required final String ywalletSeed}) {
final splSeed = ywalletSeed.split(" ");
String seed = "";
;
String passphrase = "";
if ([13, 25].contains(splSeed.length)) {
seed = splSeed.take(splSeed.length - 1).join(" ");
passphrase = ywalletSeed.replaceFirst("${seed} ", "");
} else {
seed = ywalletSeed;
}
return ZkoolSeed(seed: seed, passphrase: passphrase);
}
String seed;
String passphrase;
}
class YwalletAccountInfo {
YwalletAccountInfo({
required this.idAccount,
required this.name,
required this.seed,
required this.aindex,
required this.sk,
required this.ivk,
required this.address,
});
final int idAccount;
final String name;
final String seed;
late final zkoolSeed = ZkoolSeed.fromYwalletSeed(ywalletSeed: seed);
final int aindex;
final String sk;
final String ivk;
final String address;
static YwalletAccountInfo fromJson(final Map<String, dynamic> j) => YwalletAccountInfo(
idAccount: j['id_account'],
name: j['name'],
seed: j['seed'],
aindex: j['aindex'],
sk: j['sk'],
ivk: j['ivk'],
address: j['address'],
);
}
Future<void> migrateOldSqliteToZkool2({required final String walletName}) async {
final dbFile = p.join(await pathForWalletTypeDir(type: WalletType.zcash), "zec.db");
final migrateDb = await openDatabase(dbFile);
final outputRaw = await migrateDb.rawQuery(
"SELECT `id_account`, `name`, `seed`, `aindex`, `sk`, `ivk`, `address` FROM `accounts`",
);
final output = outputRaw.map(YwalletAccountInfo.fromJson);
final walletId = await ZcashWalletBase.getLegacyZcashAccountIdForName(walletName);
for (final ya in output) {
if (ya.idAccount != walletId) {
continue;
}
printV("migrating account: ${ya.name} - ${walletName}");
int birthHeight = await ZcashHeight.getBlockHeightByTime(DateTime(2026, 1, 1));
final accTxs = await migrateDb.rawQuery(
"SELECT height FROM transactions WHERE account = ${ya.idAccount}",
);
for (final tx in accTxs) {
final txHeight = int.tryParse(tx['height'].toString()) ?? birthHeight;
birthHeight = min(birthHeight, txHeight);
}
final accountId = await ZcashWalletBase.restoreZcashWalletFromSeed(
name: walletName,
seed: ya.zkoolSeed.seed,
passphrase: ya.zkoolSeed.passphrase,
birthHeight: birthHeight,
);
await ZcashWalletBase.saveAccountId(walletName, accountId);
return;
}
throw Exception("migration not finished (wallet name: $walletName not found)");
}
+6
View File
@@ -7,6 +7,7 @@ class ZcashNewWalletCredentials extends WalletCredentials {
required final String? passphrase,
final String? mnemonic,
final int? seedPhraseLength,
this.network = 0,
}) : super(
name: name,
password: password,
@@ -17,6 +18,7 @@ class ZcashNewWalletCredentials extends WalletCredentials {
}
String? mnemonic;
int network;
}
class ZcashFromSeedWalletCredentials extends WalletCredentials {
@@ -26,8 +28,10 @@ class ZcashFromSeedWalletCredentials extends WalletCredentials {
required final String? passphrase,
required this.seed,
required super.height,
this.network = 0,
}) : super(name: name, password: password, passphrase: passphrase);
final String? seed;
int network;
}
class ZcashFromKeysWalletCredentials extends WalletCredentials {
@@ -36,6 +40,8 @@ class ZcashFromKeysWalletCredentials extends WalletCredentials {
final String? password,
required final int? height,
required this.privateKey,
this.network = 0,
}) : super(name: name, password: password, height: height);
final String? privateKey;
int network;
}
+32 -49
View File
@@ -1,14 +1,10 @@
import 'dart:convert';
import 'dart:math';
import 'package:cw_core/amount/money.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/exceptions.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zcash/cw_zcash.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';
import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
import 'package:zkool/src/rust/api/network.dart' as zkool_network;
class PendingZcashTransaction with PendingTransaction {
PendingZcashTransaction({
@@ -21,7 +17,7 @@ class PendingZcashTransaction with PendingTransaction {
final ZcashWallet zcashWallet;
final ZcashTransactionCredentials credentials;
final String txPlan;
final zkool_pay.PcztPackage txPlan;
String? _txId;
final Money availableBalance;
@@ -32,56 +28,43 @@ class PendingZcashTransaction with PendingTransaction {
String get hex => '';
@override
Money get amount => Money.fromInt(_totalAmount, CryptoCurrency.zec);
Money get amount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance - fee;
}
return credentials.outputs
.map((final output) => output.cryptoAmount)
.reduce((final a, final b) => a + b);
}
@override
String get amountFormatted => amount.toString();
int get _totalAmount {
final isAll = credentials.outputs.fold<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance.amount.toInt();
}
return credentials.outputs.fold<int>(
0,
(final a, final b) => a + b.cryptoAmount.amount.toInt(),
);
}
@override
final Money 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(
cryptoAmount: Money.zero(CryptoCurrency.zec),
address: "${o1.address},${o2.address}",
sendAll: false,
isParsedAddress: false,
await ZcashWalletBase.runWithCoin(
accountId: zcashWallet.accountId,
func: (coin) async {
final signTx = await zkool_pay.signTransaction(pczt: txPlan, c: coin);
final txBytes = await zkool_pay.extractTransaction(package: signTx);
final currentHeight = await zkool_network.getCurrentHeight(c: coin);
final result = await zkool_pay.broadcastTransaction(
height: currentHeight,
txBytes: txBytes,
c: coin,
);
}).address,
printV("result: $result");
final txId = ZcashWalletService.normalizeTxId(result);
if (txId.length != 64) {
throw TransactionCommitFailed(errorMessage: result);
}
_txId = txId;
zcashWallet.rememberPendingOutgoingAmount(txId, amount);
},
);
await zcashWallet.updateTransactions();
await zcashWallet.updateBalance();
+5
View File
@@ -0,0 +1,5 @@
import 'package:flutter/services.dart';
String uint8ListToHex(final Uint8List bytes) {
return bytes.map((final b) => b.toRadixString(16).padLeft(2, '0')).join();
}
+135
View File
@@ -0,0 +1,135 @@
import 'dart:async';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_zcash/src/zcash_wallet_service.dart';
import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
import 'package:zkool/src/rust/api/mempool.dart' as zkool_mempool;
typedef ZcashMempoolRefreshCallback = void Function(Set<int> accountIds);
class ZcashMempoolService {
ZcashMempoolService._();
static final ZcashMempoolService instance = ZcashMempoolService._();
zkool_mempool.Mempool? _mempool;
final Map<String, zkool_mempool.MempoolTx> _txsById = {};
zkool_mempool.Mempool get _mempoolInstance => _mempool ??= zkool_mempool.Mempool();
ZcashMempoolRefreshCallback? onAccountsUpdated;
bool _loopRunning = false;
String? _nodeUrl;
int? _lastBlockHeight;
StreamSubscription<zkool_mempool.MempoolMsg>? _subscription;
void removeTx(final String txId) {
_txsById.remove(ZcashWalletService.normalizeTxId(txId));
}
void removeKnownTxs(final Iterable<String> txIds) {
for (final txId in txIds) {
removeTx(txId);
}
}
List<zkool_mempool.MempoolTx> txsForAccount(final int accountId) => _txsById.values
.where(
(final tx) =>
tx.notes.any((final n) => n.account == accountId) ||
tx.amounts.any((final a) => a.account == accountId),
)
.toList();
Future<void> ensureRunning(final zkool_coin.Coin c) async {
if (c.url.isEmpty) {
return;
}
if (_loopRunning && _nodeUrl == c.url) {
return;
}
await stop();
_nodeUrl = c.url;
_loopRunning = true;
unawaited(_runLoop(c));
}
Future<void> stop() async {
_loopRunning = false;
await _subscription?.cancel();
_subscription = null;
_txsById.clear();
_lastBlockHeight = null;
try {
await _mempool?.cancel();
} catch (e) {
printV("mempool cancel: $e");
}
_mempool = null;
}
Future<void> _runLoop(final zkool_coin.Coin c) async {
while (_loopRunning && _nodeUrl == c.url) {
try {
final completer = Completer<void>();
_subscription = _mempoolInstance
.run(c: c)
.listen(
(final msg) {
if (!_loopRunning) {
return;
}
switch (msg) {
case zkool_mempool.MempoolMsg_BlockHeight(:final field0):
_onBlockHeight(field0);
case zkool_mempool.MempoolMsg_TxId(:final field0):
_onMempoolTx(field0);
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
onError: (final e) {
printV("mempool stream error: $e");
if (!completer.isCompleted) {
completer.complete();
}
},
);
await completer.future;
} catch (e) {
printV("mempool loop error: $e");
}
if (_loopRunning) {
await Future.delayed(const Duration(seconds: 5));
}
}
}
void _onBlockHeight(final int height) {
final heightChanged = _lastBlockHeight != null && height > _lastBlockHeight!;
_lastBlockHeight = height;
if (!heightChanged) {
return;
}
final affected = _allAffectedAccounts();
_txsById.clear();
onAccountsUpdated?.call(affected);
}
void _onMempoolTx(final zkool_mempool.MempoolTx tx) {
final normalized = ZcashWalletService.normalizeTxId(tx.txid);
_txsById[normalized] = tx;
onAccountsUpdated?.call(_affectedAccountsForTx(tx));
}
Set<int> _allAffectedAccounts() => _txsById.values.expand(_affectedAccountsForTx).toSet();
Set<int> _affectedAccountsForTx(final zkool_mempool.MempoolTx tx) => {
...tx.notes.map((final n) => n.account),
...tx.amounts.map((final a) => a.account),
};
}
@@ -1,5 +0,0 @@
class ZcashMnemonicIsIncorrectException implements Exception {
@override
String toString() =>
'Zcash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.';
}
+49
View File
@@ -0,0 +1,49 @@
enum ZcashNetwork {
mainnet,
testnet,
regtest;
/// NU6.3 / Ironwood activation height on zkool regtest (see zkool2/misc/zebra.toml).
static const int regtestNu63Height = 250;
String get value => name;
String get label => switch (this) {
ZcashNetwork.mainnet => 'Mainnet',
ZcashNetwork.testnet => 'Testnet',
ZcashNetwork.regtest => 'Regtest',
};
String get dbFileName => switch (this) {
ZcashNetwork.mainnet => 'zec.v2.db',
ZcashNetwork.testnet => 'zec.testnet.v2.db',
ZcashNetwork.regtest => 'zec.regtest.v2.db',
};
/// Default lightwalletd endpoint for dev networks.
String get defaultNodeUri => switch (this) {
ZcashNetwork.mainnet => 'zec.rocks:443',
ZcashNetwork.testnet => 'testnet.lightwalletd.com:9067',
ZcashNetwork.regtest => '10.0.2.2:9067', // 10.0.2.2 - AVD host bridge
};
bool get useSsl => this == ZcashNetwork.mainnet;
static ZcashNetwork fromName(final String? name) => switch (name) {
'testnet' => ZcashNetwork.testnet,
'regtest' => ZcashNetwork.regtest,
_ => ZcashNetwork.mainnet,
};
static ZcashNetwork fromIndex(final int index) => switch (index) {
1 => ZcashNetwork.testnet,
2 => ZcashNetwork.regtest,
_ => ZcashNetwork.mainnet,
};
int get networkIndex => switch (this) {
ZcashNetwork.testnet => 1,
ZcashNetwork.regtest => 2,
ZcashNetwork.mainnet => 0,
};
}
@@ -19,7 +19,7 @@ class ZcashReceivePageOption implements ReceivePageOption {
case ZcashAddressType.shieldedSapling:
return shieldedSapling;
case ZcashAddressType.shieldedOrchard:
return shieldedOrchard;
return shieldedOrchard();
case ZcashAddressType.unifiedType:
return unified;
}
@@ -57,13 +57,18 @@ class ZcashReceivePageOption implements ReceivePageOption {
description: "Sapling",
iconPath: "assets/new-ui/address-type-picker-icons/zec/sapling.svg",
);
static const shieldedOrchard = ZcashReceivePageOption._(
ZcashAddressType.shieldedOrchard,
"Shielded",
description: "Default (Orchard)",
iconPath: "assets/new-ui/address-type-picker-icons/zec/shielded.svg",
isCommon: true,
);
static const _shieldedOrchardIcon =
"assets/new-ui/address-type-picker-icons/zec/shielded.svg";
static ZcashReceivePageOption shieldedOrchard({final bool ironwood = false}) {
return ZcashReceivePageOption._(
ZcashAddressType.shieldedOrchard,
"Shielded",
description: ironwood ? "Default (Ironwood)" : "Default (Orchard)",
iconPath: _shieldedOrchardIcon,
isCommon: true,
);
}
static const unified = ZcashReceivePageOption._(
ZcashAddressType.unifiedType,
"Unified",
@@ -78,13 +83,15 @@ class ZcashReceivePageOption implements ReceivePageOption {
return value;
}
static const all = [
ZcashReceivePageOption.shieldedOrchard,
ZcashReceivePageOption.shieldedSapling,
ZcashReceivePageOption.unified,
ZcashReceivePageOption.transparentRotated,
ZcashReceivePageOption.transparent,
];
static List<ZcashReceivePageOption> allOptionsFor({final bool ironwood = false}) => [
shieldedOrchard(ironwood: ironwood),
shieldedSapling,
unified,
transparentRotated,
transparent,
];
static List<ZcashReceivePageOption> get allOptions => allOptionsFor();
ZcashAddressType toType() {
return type;
+505 -351
View File
@@ -12,368 +12,509 @@
// turned off and replaced with proper solution.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' show max;
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.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;
import 'package:cw_zcash/src/zkool_compat.dart';
import 'package:cw_zcash/src/zkooltx.dart';
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
import 'package:zkool/src/rust/api/coin.dart' as zkool_coin;
import 'package:zkool/src/rust/api/sync.dart' as zkool_sync;
import 'package:zkool/src/rust/api/pay.dart' as zkool_pay;
import 'package:zkool/src/rust/api/network.dart' as zkool_network;
import 'package:zkool/src/rust/api/sweep.dart' as zkool_sweep;
import 'package:zkool/src/rust/pay.dart' as zkool_paydart;
class ZcashTaddressRotation {
ZcashTaddressRotation(this.accountIndex);
final int accountIndex;
static bool _isStarted = false;
static zkool_coin.Coin get c => ZcashWalletBase.c;
static const int _sweepThreshold = 30000;
static const int minSpendableNote = 5000;
static const int _lookahead = 5;
// Matches transparentLimit in ZcashWalletBase._oneshotSync.
static const int _transparentSyncLimit = 100;
static const coin = ZcashWalletBase.coin;
static String seedForOffset(final String seed) {
final seedWords = seed.split(" ");
static zkool_account.Seed seedForOffset(final zkool_account.Seed seed) {
final seedStr = "${seed.mnemonic} ${seed.phrase.replaceAll(" ", "_")}".trim();
final seedWords = seedStr.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(" ");
final crc = CRC32.compute(seedStr);
seedWords[lastI] += ":tgen:$crc";
final phrase = seedWords.removeLast();
return zkool_account.Seed(mnemonic: seedWords.join(" "), phrase: phrase, aindex: 0);
}
static bool isSeedForWallet(final String? mainWallet, final String? subWallet) {
static bool zkoolAccountSeedIsEqual(final zkool_account.Seed a, final zkool_account.Seed b) {
return a.mnemonic == b.mnemonic && a.phrase == b.phrase;
}
static bool isSeedForWallet(
final zkool_account.Seed? mainWallet,
final zkool_account.Seed? subWallet,
) {
if (mainWallet == null || subWallet == null) return false;
return seedForOffset(mainWallet.trim()) == subWallet;
return zkoolAccountSeedIsEqual(seedForOffset(mainWallet), subWallet);
}
static Map<int, List<ZkoolTx>> shieldedAccountsTx = {};
static final Map<int, int> _rotationAccountByMain = {};
static final Set<int> _resyncedRotationAccounts = {};
static final Set<String> _poolRescanAttempted = {};
static bool _sweepJobRunning = false;
static final Map<int, DateTime> _lastSweepBroadcastAt = {};
static bool _poolNeedsRescan(final List<zkool_account.TAddressTxCount> slots) {
if (slots.isEmpty) {
return false;
}
final unused = slots.where((final e) => e.txCount == 0);
if (unused.isEmpty) {
return false;
}
final head = unused.first;
final tail = slots.last.dindex;
if (slots.any((final e) => e.dindex > head.dindex && e.txCount > 0)) {
return true;
}
return head.dindex + _transparentSyncLimit <= tail;
}
static Future<void> _scanTransparentChain(final int rotationAccountId) async {
await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) async {
final all = await zkool_account.fetchAddressTxCount(
c: coin,
aggregate: false,
poolFilter: 1,
);
final external = all.where((final e) => e.scope == 0).toList();
if (external.isEmpty) {
return;
}
final maxD = external.map((final e) => e.dindex).reduce(max);
final scanner = await zkool_sweep.TransparentScanner.newInstance();
final height = await zkool_network.getCurrentHeight(c: coin);
final completer = Completer<void>();
final sub = scanner
.run(endHeight: height, gapLimit: maxD + _lookahead + 1, c: coin)
.listen(
(_) {},
onError: (final e) {
printV("transparent chain scan: $e");
if (!completer.isCompleted) {
completer.complete();
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
);
await Future.any([
completer.future,
Future.delayed(Duration(seconds: (maxD + 2).clamp(8, 45))),
]);
await sub.cancel();
},
);
}
static Future<void> _resyncRotationPool(
final int rotationAccountId,
final int transparentLimit,
) async {
await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) async {
await zkool_sync.cancelSync();
await zkool_account.resetSync(id: rotationAccountId, c: coin);
final height = await zkool_network.getCurrentHeight(c: coin);
final sync = zkool_sync.synchronize(
accounts: [rotationAccountId],
currentHeight: height,
actionsPerSync: 10000,
transparentLimit: transparentLimit,
checkpointAge: 200,
c: coin,
fast: false,
);
final completer = Completer<void>();
sync.listen(
(_) {},
onError: (final e) {
printV("rotation pool resync error: $e");
if (!completer.isCompleted) {
completer.complete();
}
},
onDone: () {
if (!completer.isCompleted) {
completer.complete();
}
},
);
await completer.future;
},
);
}
static Future<void> _ensurePoolScanned(final int rotationAccountId) async {
var slots = await _transparentSlots(rotationAccountId);
final firstSession = _resyncedRotationAccounts.add(rotationAccountId);
if (!firstSession && !_poolNeedsRescan(slots)) {
return;
}
if (!firstSession) {
final head = slots.where((final e) => e.txCount == 0).firstOrNull?.dindex;
final key = '$rotationAccountId:$head';
if (_poolRescanAttempted.contains(key)) {
return;
}
_poolRescanAttempted.add(key);
}
await _ensureLookahead(rotationAccountId);
await _scanTransparentChain(rotationAccountId);
slots = await _transparentSlots(rotationAccountId);
final limit = slots.isEmpty ? _transparentSyncLimit : slots.last.dindex + 1;
printV("rotation pool resync account $rotationAccountId limit $limit");
await _resyncRotationPool(rotationAccountId, limit);
}
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));
await Future.delayed(const Duration(seconds: 2));
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));
static Future<int?> getRotationAccountForMainAccount(final int mainAccountId) async {
final cached = _rotationAccountByMain[mainAccountId];
if (cached != null) {
return cached;
}
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(),
),
await ZcashWalletBase.runWithCoinMutex.acquire();
final wSeed = await zkool_account.getAccountSeed(c: c, account: mainAccountId);
if (wSeed == null) {
printV("Not running Taddr rotation - seed not found");
return null;
}
final seed = seedForOffset(wSeed);
final accs = await zkool_account.listAccounts(c: c);
for (int i = 0; i < accs.length; i++) {
final accSeed = await zkool_account.getAccountSeed(c: c, account: accs[i].id);
if (accSeed?.mnemonic == seed.mnemonic &&
accSeed?.phrase == seed.phrase &&
accSeed?.aindex == seed.aindex) {
_rotationAccountByMain[mainAccountId] = accs[i].id;
return accs[i].id;
}
}
zkool_account.Account? acc;
for (final a in accs) {
if (a.id == mainAccountId) {
acc = a;
break;
}
}
if (acc == null) {
printV("Main account $mainAccountId not found for Taddr rotation");
return null;
}
final name = CRC32.compute(wSeed.mnemonic).toString();
final id = await newAccount(
name: name,
seed: seed.mnemonic,
passphrase: seed.phrase,
height: acc.birth,
aindex: seed.aindex,
);
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;
_rotationAccountByMain[mainAccountId] = id;
return id;
} finally {
ZcashWalletBase.runWithCoinMutex.release();
}
}
static Uint8List atob(final String value) =>
Uint8List.fromList(List<int>.from(base64.decode(value)));
// External (scope 0) transparent slots for the rotation account, ordered by
// dindex. zkool derives every slot in 0..=dindex, so unused ones (txCount==0)
// form the disposable look-ahead pool.
static Future<List<zkool_account.TAddressTxCount>> _transparentSlots(
final int rotationAccountId,
) async {
final all = await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) =>
zkool_account.fetchAddressTxCount(c: coin, aggregate: false, poolFilter: 1),
);
return all.where((final e) => e.scope == 0).toList()
..sort((final a, final b) => a.dindex.compareTo(b.dindex));
}
// Keep _lookahead unused disposable addresses ready, generating more whenever
// the pool runs low. Newly generated addresses are scanned by ongoing sync
// (the latest addresses are always in the scan window), so no resync needed.
static Future<void> _ensureLookahead(final int rotationAccountId) async {
final slots = await _transparentSlots(rotationAccountId);
final unused = slots.where((final e) => e.txCount == 0).length;
final toGenerate = _lookahead - unused;
for (var i = 0; i < toGenerate; i++) {
await ZcashWalletBase.runWithCoin(
accountId: rotationAccountId,
func: (final coin) => zkool_account.generateNextDindex(c: coin),
);
}
if (toGenerate > 0) {
printV("topped up disposable taddr pool by $toGenerate (had $unused)");
}
}
static bool _isWalletSynced(final int mainAccountId) {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
return wallet != null && wallet.syncStatus is SyncedSyncStatus;
}
static Future<BigInt> _rotationTransparentBalance(final int rotationAccount) async {
return ZcashWalletBase.runWithCoin(
accountId: rotationAccount,
func: (final coin) async {
final b = await zkool_sync.balance(c: coin);
return b.field0.elementAt(0);
},
);
}
static Future<List<ZkoolTx>> _loadTxHistory(final int accountId) async {
return ZcashWalletBase.runWithCoin(
accountId: accountId,
func: (final coin) async {
final txsI = await zkool_account.listTxHistory(c: coin);
final txs = <ZkoolTx>[];
for (int i = 0; i < txsI.length; i++) {
final tx = txsI[i];
try {
final details = await zkool_account.getTxDetails(idTx: tx.id, c: coin);
txs.add(ZkoolTx(tx, details));
} catch (e) {
printV("getTxDetails skipped for tx ${tx.id}: $e");
}
}
return txs;
},
);
}
static bool _canBroadcastSweep(final int mainAccountId) {
final lastSweep = _lastSweepBroadcastAt[mainAccountId];
if (lastSweep == null) {
return true;
}
return !lastSweep.isAfter(DateTime.now().subtract(const Duration(seconds: 120)));
}
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})",
);
if (_sweepJobRunning) {
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);
if (backup.seed == null) continue;
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] = [];
_sweepJobRunning = true;
try {
final mainAccountIds = ZcashWalletBase.walletsByAccountId.keys.toList();
if (mainAccountIds.isEmpty) {
await createAndSweepTAddressesForAccount(c.account);
return;
}
}
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;
});
// remove hidden addresses
final wis = await WalletInfo.selectList('type = ?', [WalletType.zcash.index]);
final List<String> hiddenAddresses_ = [];
for (int k = 0; k < wis.length; k++) {
final addrs = await wis[k].getHiddenAddresses();
final addr2 = await wis[k].getUsedAddresses();
hiddenAddresses_.addAll(addrs);
hiddenAddresses_.addAll(addr2);
}
final Set<String> hiddenAddresses = hiddenAddresses_.toSet();
rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) {
return hiddenAddresses.contains(WarpApi.getTAddr(coin, a.id));
});
}
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}");
});
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;
for (final mainAccountId in mainAccountIds) {
try {
bal += await WarpApi.getTBalance(coin, accs[j].id);
} catch (e) {
printV("getTBalance: $e");
return;
await createAndSweepTAddressesForAccount(mainAccountId);
} catch (e, s) {
printV("createAndSweepTAddresses for $mainAccountId error: $e");
s.toString().split("\n").forEach(printV);
}
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,
);
}
} finally {
_sweepJobRunning = false;
}
}
final recipient = Recipient(recipientBuilder.toBytes());
final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0);
await ZcashWalletBase.loadProver();
final txPlan = await ZcashWalletService.runInDbMutex(
() => WarpApi.prepareTx(
coin,
accs[j].id,
[recipient],
3, // pools: (Transparent + Sapling)
1,
0, // anchorOffset
fee,
static Future<void> createAndSweepTAddressesForAccount(final int cId) async {
if (!_isWalletSynced(cId)) {
printV("rotation sweep waiting for wallet sync (account $cId)");
return;
}
printV("createAndSweepTAddresses for account $cId");
final rotationAccount = await getRotationAccountForMainAccount(cId);
if (rotationAccount == null) {
printV("rotation account is null, bailing out");
return;
}
await _ensureLookahead(rotationAccount);
await _ensurePoolScanned(rotationAccount);
await _refreshWalletAddresses(cId);
BigInt transparentBal = BigInt.zero;
try {
transparentBal = await _rotationTransparentBalance(rotationAccount);
printV("rotation account $rotationAccount transparent balance: $transparentBal");
} catch (e) {
printV("getTBalance: $e");
return;
}
if (transparentBal < BigInt.from(_sweepThreshold)) {
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
return;
}
if (!_canBroadcastSweep(cId)) {
printV("rotation sweep rate-limited for account $cId");
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
return;
}
final toAddress = await ZcashWalletBase.runWithCoin(
accountId: cId,
func: (final coin) async {
final addrs = await zkool_account.getAddresses(c: coin, uaPools: 7);
final orchard = addrs.oaddr;
if (orchard == null || orchard.isEmpty) {
throw Exception('Orchard address unavailable for rotation sweep');
}
return orchard;
},
);
final result = await ZcashWalletBase.runWithCoin(
accountId: rotationAccount,
func: (final coin) async {
final height = await zkool_network.getCurrentHeight(c: coin);
final notes = await zkool_account.listNotes(c: coin);
var amount = BigInt.zero;
for (final note in notes) {
if (note.pool != NotePool.transparent.index || note.locked) {
continue;
}
if (note.value < BigInt.from(minSpendableNote)) {
continue;
}
if (note.height > height) {
continue;
}
amount += note.value;
}
if (amount < BigInt.from(_sweepThreshold)) {
throw Exception('rotation sweep: insufficient spendable transparent notes');
}
final ironwood = await zkool_network.isIronwoodActive(c: coin);
final tx = await zkool_pay.prepare(
recipients: [
zkool_paydart.Recipient(
assetBase: zecBase,
address: toAddress,
amount: amount,
pools: ironwood ? ironwoodPoolMask : null,
),
],
options: zkool_pay.PaymentOptions(
srcPools: 1,
recipientPaysFee: true,
smartTransparent: false,
mode: 0,
),
c: coin,
);
final _txId = await ZcashWalletService.runInDbMutex(
() => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accs[j].id, txPlan),
);
await ZcashWalletService.addShieldedTx(_txId);
printV("shielded: $_txId");
final signTx = await zkool_pay.signTransaction(pczt: tx, c: coin);
final txBytes = await zkool_pay.extractTransaction(package: signTx);
return zkool_pay.broadcastTransaction(height: height, txBytes: txBytes, c: coin);
},
);
printV("rotation sweep broadcast: $result");
if (result.isEmpty) {
throw Exception("rotation sweep broadcast failed");
}
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;
await ZcashWalletService.addShieldedTx(result);
_lastSweepBroadcastAt[cId] = DateTime.now();
await updateCache(mainAccountId: cId);
await _refreshWalletAfterRotationChange(cId);
}
static ShieldedTx reverseAmount(final ShieldedTx t) {
final tt = t.unpack();
tt.value = -tt.value;
return ShieldedTx(base64.decode(flatBuffersPack(tt.pack)));
static Future<void> _refreshWalletAfterRotationChange(final int mainAccountId) async {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
if (wallet == null) {
return;
}
unawaited(wallet.updateTransactions());
unawaited(wallet.updateBalance());
}
static Account? accountForSeed(final String seed) {
final accounts = WarpApi.getAccountList(coin);
static Future<void> updateCache({required final int mainAccountId}) async {
final rotationAccount = await getRotationAccountForMainAccount(mainAccountId);
if (rotationAccount == null) {
printV("rotationAccount is null");
return;
}
await _ensureLookahead(rotationAccount);
await _ensurePoolScanned(rotationAccount);
final txs = await _loadTxHistory(rotationAccount);
shieldedAccountsTx[mainAccountId] = txs;
if (mainAccountId != rotationAccount && shieldedAccountsTx.containsKey(rotationAccount)) {
shieldedAccountsTx.remove(rotationAccount);
}
for (final tx in shieldedAccountsTx[mainAccountId]!) {
if (tx.direction == TransactionDirection.outgoing) {
await ZcashWalletService.addShieldedTx(tx.txHash);
}
}
await _refreshWalletAddresses(mainAccountId);
await _refreshWalletAfterRotationChange(mainAccountId);
}
static Future<void> _refreshWalletAddresses(final int mainAccountId) async {
final wallet = ZcashWalletBase.walletsByAccountId[mainAccountId];
if (wallet == null) {
return;
}
try {
await wallet.walletAddresses.refreshRotationAddresses();
} catch (e) {
printV("rotation address refresh: $e");
}
}
static List<ZkoolTx> rotationTxsForMainAccount(final int mainAccountId) {
return shieldedAccountsTx[mainAccountId] ?? const [];
}
static Future<zkool_account.Account?> accountForSeed(final zkool_account.Seed seed) async {
final accounts = await zkool_account.listAccounts(c: c);
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) {
final backup = await zkool_account.getAccountSeed(c: c, account: acc.id);
if (backup?.mnemonic == seed.mnemonic && backup?.phrase == seed.phrase) {
return acc;
}
}
@@ -383,63 +524,76 @@ class ZcashTaddressRotation {
static Future<void> _jobRunner() async {
for (;;) {
try {
await Future.delayed(Duration(seconds: 5));
await Future.delayed(const Duration(seconds: 5));
await createAndSweepTAddresses();
} catch (e, s) {
printV(e);
printV("createAndSweepTAddresses error: $e");
s.toString().split("\n").forEach(printV);
rethrow;
} finally {
await Future.delayed(Duration(seconds: 30));
await Future.delayed(const Duration(seconds: 30));
}
}
}
static String? addressForAccount(final int accountId, final Set<String>? hiddenAddresses) {
final accs = rotationAccountsUsable[accountId] ?? [];
for (int i = 0; i < accs.length; i++) {
final addr = WarpApi.getTAddr(coin, accs[i].id);
if (hiddenAddresses?.contains(addr) == true) {
continue;
}
return addr;
}
return null;
}
static List<String>? allAddressesForAccount(final int accountId) {
final acc = rotationAccounts[accountId];
if (acc == null) {
printV("Nothing found");
// Current disposable address = first unused slot (lowest dindex).
static Future<String?> addressForAccount(final int accountId) async {
final rotationAccount = await getRotationAccountForMainAccount(accountId);
if (rotationAccount == null) {
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)}");
final slots = await _transparentSlots(rotationAccount);
final unused = slots.where((final e) => e.txCount == 0);
if (unused.isNotEmpty) {
return unused.first.address;
}
return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
return slots.isNotEmpty ? slots.last.address : null;
}
static List<String>? allUsedAddressesForAccount(final int accountId) {
final seed = WarpApi.getBackup(coin, accountId).seed;
if (seed == null) return [];
final acc = rotationAccounts[seed]?.toList();
if (acc == null) {
printV("Nothing found");
return null;
static Future<List<String>> usedAddressesForAccount(final int accountId) async {
final rotationAccount = await getRotationAccountForMainAccount(accountId);
if (rotationAccount == null) {
return const [];
}
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)}");
final slots = await _transparentSlots(rotationAccount);
return [
for (final entry in slots)
if (entry.txCount > 0) entry.address,
];
}
// The full disposable pool: used slots plus the unused look-ahead window.
static Future<List<String>> allAddressesForAccount(final int accountId) async {
final rotationAccount = await getRotationAccountForMainAccount(accountId);
if (rotationAccount == null) {
return const [];
}
return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList();
final slots = await _transparentSlots(rotationAccount);
return slots.map((final e) => e.address).toList();
}
static Future<int> newAccount({
required final String name,
required final String seed,
required final String passphrase,
required final int height,
required final int aindex,
}) async {
final id = await zkool_account.newAccount(
na: zkool_account.NewAccount(
name: name,
restore: true,
passphrase: passphrase,
key: seed,
aindex: aindex,
birth: height,
folder: '',
pools: 1,
useInternal: true,
internal: false,
ledger: false,
),
c: c,
);
return id;
}
}
+16 -50
View File
@@ -1,15 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'package:cw_core/amount/money.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;
import 'package:cw_zcash/src/zkooltx.dart';
class ZcashTransactionInfo extends TransactionInfo {
ZcashTransactionInfo({
@@ -23,6 +17,10 @@ class ZcashTransactionInfo extends TransactionInfo {
required final int confirmations,
required final String to,
final String? memo,
final TxType? txType,
final bool isRotationReceive = false,
final bool isShieldAction = false,
final bool isIronwoodMigration = false,
}) {
this.id = id;
this.amount = amount;
@@ -32,23 +30,22 @@ class ZcashTransactionInfo extends TransactionInfo {
this.date = date;
this.isPending = isPending;
this.confirmations = confirmations;
this.to = getCachedDestinationAddress(id);
this.to = to;
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;
if (txType != null) {
additionalInfo['txType'] = txType.name;
}
// remove below when above starts working
additionalInfo['autoShield'] = ZcashWalletService.autoshieldTx.contains(txHash);
// --- == === cut here === == ---
if (additionalInfo['autoShield'] == true) {
additionalInfo['isRotationReceive'] = isRotationReceive;
additionalInfo['isAutoShield'] = isShieldAction || ZcashWalletService.isAutoshieldTx(txHash);
additionalInfo['isIronwoodMigration'] = isIronwoodMigration;
if (additionalInfo['isAutoShield'] == true) {
additionalInfo['memo'] ??= '';
additionalInfo['memo'] += '\This is an auto-shielding transaction. Enjoy default privacy!';
additionalInfo['memo'] = additionalInfo['memo'].trim();
additionalInfo['memo'] =
'${additionalInfo['memo']}\nThis is an auto-shielding transaction. Enjoy default privacy!.'
.trim();
}
}
@@ -61,35 +58,4 @@ class ZcashTransactionInfo extends TransactionInfo {
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) {
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");
}
}
}
File diff suppressed because it is too large Load Diff
+141 -100
View File
@@ -6,7 +6,7 @@ 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';
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
part 'zcash_wallet_addresses.g.dart';
@@ -17,24 +17,50 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? "");
}
final int accountId;
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;
@observable
String? transparentAddress;
@observable
String? _transparentObservableAddress;
@observable
String? saplingAddress;
@observable
String? orchardAddress;
@observable
String? unifiedAddress;
@observable
bool ironwoodActive = false;
@action
void setIronwoodActive(final bool active) {
ironwoodActive = active;
}
@override
@computed
List<ReceivePageOption> get receivePageOptions {
return [
...ZcashReceivePageOption.allOptionsFor(ironwood: ironwoodActive),
...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet),
];
}
@override
String get latestAddress {
switch (addressPageType) {
case ZcashAddressType.transparent:
return transparentAddress;
return transparentAddress ?? "unknown 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
@@ -46,16 +72,15 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
// 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;
return _transparentObservableAddress ??
transparentAddress ??
"unknown transparentAddressRotated";
case ZcashAddressType.shieldedSapling:
return saplingAddress;
return saplingAddress ?? "unknown saplingAddress";
case ZcashAddressType.shieldedOrchard:
return orchardAddress;
return orchardAddress ?? "unknown orchardAddress";
case ZcashAddressType.unifiedType:
if (_unifiedAddress == null) {
_unifiedAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 6);
}
return _unifiedAddress ?? "";
return unifiedAddress ?? "unknown unifiedAddress";
}
}
@@ -79,69 +104,27 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
await walletInfo.save();
}
String get transparentAddress {
try {
if (_transparentAddress == null) {
_transparentAddress = WarpApi.getTAddr(ZcashWalletBase.coin, accountId);
}
return _transparentAddress ?? "";
} catch (e) {
return "";
}
}
@override
@computed
String? get transparentAddressRotated {
try {
return ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
} 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 "";
}
}
String get addressForExchange =>
_transparentObservableAddress ?? transparentAddress ?? "unknown addressForExchange";
@override
@computed
String get addressForExchange => transparentAddressRotated ?? transparentAddress;
@override
@computed
String get addressForBuy => transparentAddressRotated ?? transparentAddress;
String get addressForBuy =>
_transparentObservableAddress ?? transparentAddress ?? "unknown addressForBuy";
@override
bool containsAddress(final String address) {
return this.address == address || addressesMap.values.contains(address);
}
static int get coin => ZcashWalletBase.coin;
@override
@observable
Set<String> hiddenAddresses = {};
Future<void> _initAddresses() async {
addressesMap = await walletInfo.getAddresses();
addressesMap = {};
addressInfos = await walletInfo.getAddressInfos();
usedAddresses = await walletInfo.getUsedAddresses();
manualAddresses = await walletInfo.getManualAddresses();
@@ -149,37 +132,105 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
address = latestAddress;
}
Future<void> _syncRotationHiddenAddresses() async {
final all = (await ZcashTaddressRotation.allAddressesForAccount(accountId)).toSet();
final used = (await ZcashTaddressRotation.usedAddressesForAccount(accountId)).toSet();
hiddenAddresses.removeWhere(all.contains);
hiddenAddresses.addAll(used);
}
@override
Future<void> init() async {
try {
await _init();
} catch (e) {
printV("init failed, retrying in 2 seconds");
printV(e);
await Future.delayed(Duration(seconds: 2));
await init();
}
}
static bool isPlaceholderAddress(final String? address) =>
address == null || address.isEmpty || address.startsWith('unknown ');
String _resolvedWalletInfoAddress() {
for (final candidate in [unifiedAddress, orchardAddress, saplingAddress, transparentAddress]) {
if (!isPlaceholderAddress(candidate)) {
return candidate!;
}
}
return latestAddress;
}
Future<void> _init() async {
await _initAddresses();
await ZcashTaddressRotation.init();
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.addAll(
ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {},
final addr = await ZcashWalletBase.runWithCoin(
accountId: accountId,
func: (final coin) => zkool_account.getAddresses(c: coin, uaPools: 7),
);
// addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address));
transparentAddress = addr.taddr ?? 'unknown addr.taddr';
saplingAddress = addr.saddr ?? 'unknown addr.saddr';
orchardAddress = addr.oaddr ?? 'unknown addr.oaddr';
unifiedAddress = addr.ua ?? 'unknown addr.ua';
_transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
addressInfos = {
0: [
for (int i = 0; i < rotationAddrs.length; i++)
WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: i + 1,
accountIndex: 0,
address: rotationAddrs[i],
label: "",
),
],
};
await _syncRotationHiddenAddresses();
await _applyAddressForCurrentType();
if (isPlaceholderAddress(walletInfo.address)) {
final resolved = _resolvedWalletInfoAddress();
if (!isPlaceholderAddress(resolved)) {
walletInfo.address = resolved;
await walletInfo.save();
}
}
await saveAddressesInBox();
}
Future<void> refreshRotationAddresses() async {
_transparentObservableAddress = await ZcashTaddressRotation.addressForAccount(accountId);
final rotationAddrs = await ZcashTaddressRotation.allAddressesForAccount(accountId);
addressInfos = {
0: [
for (int i = 0; i < rotationAddrs.length; i++)
WalletInfoAddressInfo(
walletInfoId: walletInfo.internalId,
mapKey: i + 1,
accountIndex: 0,
address: rotationAddrs[i],
label: "",
),
],
};
await _syncRotationHiddenAddresses();
await _applyAddressForCurrentType();
}
Future<void> _applyAddressForCurrentType() async {
if (_addressPageType == ZcashAddressType.transparentRotated) {
final addr = ZcashTaddressRotation.addressForAccount(accountId, hiddenAddresses);
final addr = await ZcashTaddressRotation.addressForAccount(accountId);
if (addr != null) {
address = addr;
}
return;
}
await saveAddressesInBox();
address = latestAddress;
}
@observable
@@ -198,12 +249,17 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> saveAddressesInBox() async {
try {
await walletInfo.setAddresses(addressesMap);
if (!isPlaceholderAddress(address)) {
walletInfo.address = address;
}
await walletInfo.setAddresses({});
await walletInfo.setAddressInfos(addressInfos);
await walletInfo.setUsedAddresses(usedAddresses.toList());
await walletInfo.setHiddenAddresses(hiddenAddresses.toList());
await walletInfo.setManualAddresses(manualAddresses.toList());
await walletInfo.save();
await _initAddresses();
await _syncRotationHiddenAddresses();
} catch (e) {
printV("Error saving addresses: $e");
}
@@ -211,10 +267,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
@override
Future<void> updateAddressesInBox() async {
_transparentAddress = null;
_saplingAddress = null;
_orchardAddress = null;
_unifiedAddress = null;
await _initAddresses();
}
@override
@@ -227,19 +280,7 @@ abstract class ZcashWalletAddressesBase extends WalletAddresses with Store {
if (addressPageType != ZcashAddressType.transparentRotated) {
return [];
}
final List<WalletInfoAddressInfo> allInfos = [];
for (final entry in addressInfos.entries) {
allInfos.addAll(entry.value);
}
return allInfos;
}
@override
List<ReceivePageOption> get receivePageOptions {
return [
...ZcashReceivePageOption.all,
...ReceivePageOptions.where((final element) => element != ReceivePageOption.mainnet),
];
return addressInfos[0] ?? [];
}
@override
+34 -21
View File
@@ -9,6 +9,7 @@ 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:cw_zcash/src/migrate.dart';
import 'package:mutex/mutex.dart';
import 'package:path/path.dart' as p;
@@ -25,9 +26,9 @@ class ZcashWalletService
static Mutex dbMutex = Mutex();
static int dbMutexQueue = 0;
static Future<T> runInDbMutex<T>(final Future<T> Function() call) async {
dbMutexQueue++;
await dbMutex.acquire();
try {
printV("dbMutexQueue: ${dbMutexQueue++}");
await dbMutex.acquire();
return await call();
} finally {
dbMutex.release();
@@ -37,10 +38,15 @@ class ZcashWalletService
static Set<String> autoshieldTx = {};
static String normalizeTxId(final String txId) =>
txId.replaceAll(RegExp(r'[^a-fA-F0-9]'), '').toLowerCase();
static bool isAutoshieldTx(final String txHash) => autoshieldTx.contains(normalizeTxId(txHash));
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 shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
autoshieldTx.add(normalizeTxId(txId));
final shieldedList = File(shieldedListFile);
shieldedList.writeAsStringSync(autoshieldTx.join("\n"));
}
@@ -48,12 +54,16 @@ class ZcashWalletService
static Future<void> loadShieldTxs() async {
try {
final pathForWalletType = await pathForWalletTypeDir(type: type);
final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt");
final shieldedListFile = p.join(pathForWalletType, "autoshield_list.v2.txt");
final shieldedList = File(shieldedListFile);
if (!shieldedList.existsSync()) {
return;
}
autoshieldTx = shieldedList.readAsLinesSync().toSet();
autoshieldTx = shieldedList
.readAsLinesSync()
.map(normalizeTxId)
.where((final line) => line.isNotEmpty)
.toSet();
} catch (e) {
printV("loadShieldTxs failed: $e");
}
@@ -74,17 +84,26 @@ class ZcashWalletService
@override
Future<bool> isWalletExit(final String name) async {
final path = await pathForWallet(name: name, type: getType());
return File(path).existsSync();
final oldPath = (await pathForWallet(name: name, type: getType()));
final path = (await pathForWallet(name: name, type: getType())) + ".v2";
return File(path).existsSync() || File(oldPath).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');
}
await ZcashWalletBase.$init(network: ZcashWalletBase.networkFor(walletInfo));
if (await isWalletExit(name)) {
final path = (await pathForWallet(name: name, type: getType())) + ".v2";
if (!File(path).existsSync()) {
await migrateOldSqliteToZkool2(walletName: name);
}
}
await loadShieldTxs();
try {
final wallet = await ZcashWalletBase.open(
name: name,
@@ -108,7 +127,7 @@ class ZcashWalletService
@override
Future<void> remove(final String wallet) async {
final path = await pathForWalletDir(name: wallet, type: getType());
final path = (await pathForWalletDir(name: wallet, type: getType()));
final file = Directory(path);
final isExist = file.existsSync();
@@ -129,17 +148,11 @@ class ZcashWalletService
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');
if (!await isWalletExit(currentName)) {
throw Exception('Wallet not found');
}
final currentWallet = ZcashWallet(
currentWalletInfo,
await currentWalletInfo.getDerivationInfo(),
accountId: accountId,
);
await currentWallet.renameWalletFiles(newName);
await ZcashWalletBase.renameWalletFilesForName(fromName: currentName, toName: newName);
final newWalletInfo = currentWalletInfo;
newWalletInfo.id = WalletBase.idFor(newName, getType());
@@ -162,11 +175,11 @@ class ZcashWalletService
final bool? isTestnet,
}) async {
if (credentials.seed == null || credentials.seed!.isEmpty) {
throw ZcashMnemonicIsIncorrectException();
throw Exception("Seed is missing");
}
if (!bip39.validateMnemonic(credentials.seed!)) {
throw ZcashMnemonicIsIncorrectException();
throw Exception("Seed is not valid bip39");
}
return ZcashWalletBase.restore(credentials);
+11
View File
@@ -0,0 +1,11 @@
import 'dart:typed_data';
final zecBase = Uint8List(32);
/// Whether NU6.3 (Ironwood) is active at the chain tip. Updated on balance refresh.
bool ironwoodActive = false;
/// Pool bitmask for Ironwood (pool index 3). Orchard and Ironwood share the same address.
const ironwoodPoolMask = 8;
int? get shieldedRecipientPools => ironwoodActive ? ironwoodPoolMask : null;
+337
View File
@@ -0,0 +1,337 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:cw_core/get_height_by_date_zec.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_zcash/src/util/hex.dart';
import 'package:zkool/src/rust/api/account.dart' as zkool_account;
enum TxType {
unknown,
selfTransfer, // 0
receive, // 1
sent, // 2
unshield, // 4
shield, // 8
transparentSelfTransfer, // 12
}
enum NotePool { transparent, sapling, orchard, ironwood }
class ZkoolTx {
ZkoolTx(final zkool_account.Tx tx, final zkool_account.TxAccount txAccount)
: this._tx = tx,
this._txAccount = txAccount;
final zkool_account.Tx _tx;
final zkool_account.TxAccount _txAccount;
int get height => max(
_tx.height,
_txAccount.height,
); // sometimes returns 0 - higher number would be correct in this case
DateTime get time {
final ts = max(_tx.time, _txAccount.time) * 1000;
if (ts == 0) {
return ZcashHeight.getTimeByBlockHeight(height);
}
return DateTime.fromMillisecondsSinceEpoch(ts);
}
String? get to => _txAccount.outputs.firstOrNull?.address;
Iterable<String> get outputAddresses =>
_txAccount.outputs.map((final o) => o.address).whereType<String>();
Iterable<({String address, int pool, BigInt value})> get outputsWithAddress => _txAccount.outputs
.where((final o) => o.address.isNotEmpty)
.map((final o) => (address: o.address, pool: o.pool, value: o.value));
Iterable<int> get spendPools => _txAccount.spends.map((final s) => s.pool);
Iterable<int> get notePools => _txAccount.notes.map((final n) => n.pool);
String? get memo => _txAccount.memos.firstOrNull?.memo;
BigInt get transparentOrSaplingSpent {
if (_txAccount.spends.isEmpty) {
return BigInt.zero;
}
return _txAccount.spends
.where(
(final s) => s.pool == NotePool.transparent.index || s.pool == NotePool.sapling.index,
)
.fold(BigInt.zero, (final a, final s) => a + s.value);
}
BigInt get orchardReceived {
final fromNotes = _txAccount.notes
.where((final n) => n.pool == NotePool.orchard.index)
.fold(BigInt.zero, (final a, final n) => a + n.value);
if (fromNotes > BigInt.zero) {
return fromNotes;
}
return _txAccount.outputs
.where((final o) => o.pool == NotePool.orchard.index)
.fold(BigInt.zero, (final a, final o) => a + o.value);
}
BigInt get ironwoodReceived {
final fromNotes = _txAccount.notes
.where((final n) => n.pool == NotePool.ironwood.index)
.fold(BigInt.zero, (final a, final n) => a + n.value);
if (fromNotes > BigInt.zero) {
return fromNotes;
}
return _txAccount.outputs
.where((final o) => o.pool == NotePool.ironwood.index)
.fold(BigInt.zero, (final a, final o) => a + o.value);
}
BigInt get orchardSpent => _txAccount.spends
.where((final s) => s.pool == NotePool.orchard.index)
.fold(BigInt.zero, (final a, final s) => a + s.value);
String get txHash {
final reversed = Uint8List.fromList(_txAccount.txid.reversed.toList());
final txId = uint8ListToHex(reversed);
return txId;
}
BigInt get value => _value.abs();
BigInt get fee => value - _calcValue;
BigInt get _value {
if (_tx.value != 0) {
return BigInt.from(_tx.value);
}
return _calcValue;
}
/////////// calc is short for calculator (calculated in this case)
BigInt get _calcValue {
final noteSum = _txAccount.notes.isEmpty
? BigInt.from(0)
: _txAccount.notes.map((final note) => note.value).reduce((final a, final b) => a + b);
final outputSum = _txAccount.outputs.isEmpty
? BigInt.from(0)
: _txAccount.outputs
.map((final output) => output.value)
.reduce((final a, final b) => a + b);
final spentSum = _txAccount.spends.isEmpty
? BigInt.from(0)
: _txAccount.spends.map((final spent) => spent.value).reduce((final a, final b) => a + b);
return noteSum - outputSum - spentSum;
}
TransactionDirection get direction {
switch (type) {
case TxType.selfTransfer:
case TxType.receive:
case TxType.unshield:
case TxType.shield:
case TxType.transparentSelfTransfer:
return TransactionDirection.incoming;
case TxType.sent:
return TransactionDirection.outgoing;
case TxType.unknown:
return _value > BigInt.from(0)
? TransactionDirection.incoming
: TransactionDirection.outgoing;
}
}
TxType get type {
switch (_tx.tpe) {
case 0:
return TxType.selfTransfer;
case 1:
return TxType.receive;
case 2:
return TxType.sent;
case 4:
return TxType.unshield;
case 8:
return TxType.shield;
case 12:
return TxType.transparentSelfTransfer;
default:
return TxType.unknown;
}
}
Map<String, dynamic> toJson() => {
"tx": {
"id": _tx.id,
"txid": base64.encode(_tx.txid),
"height": _tx.height,
"time": _tx.time,
"value": value.toInt(),
"tpe": _tx.tpe,
"zsaValue": _tx.zsaValue,
"assetDisplay": _tx.assetDisplay,
},
"txaccount": {
"id": _txAccount.id,
"account": _txAccount.account,
"txid": base64.encode(_txAccount.txid),
"height": _txAccount.height,
"time": _txAccount.time,
"notes": _txAccount.notes
.map(
(final note) => {
"id": note.id,
"pool": note.pool,
"height": note.height,
"tx": note.tx,
"scope": note.scope,
"value": note.value.toString(),
"locked": note.locked,
},
)
.toList(),
"spends": _txAccount.spends
.map(
(final spend) => {
"id": spend.id,
"pool": spend.pool,
"height": spend.height,
"value": spend.value.toString(),
"assetDisplay": spend.assetDisplay,
},
)
.toList(),
"outputs": _txAccount.outputs
.map(
(final output) => {
"id": output.id,
"pool": output.pool,
"height": output.height,
"value": output.value.toString(),
"address": output.address,
},
)
.toList(),
"memos": _txAccount.memos
.map(
(final memo) => {
"note": memo.note,
"pool": memo.pool,
"output": memo.output,
"memo": memo.memo,
},
)
.toList(),
},
};
static int _asInt(final dynamic value, {final int fallback = 0}) {
if (value == null) {
return fallback;
}
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.parse(value.toString());
}
static Uint8List _txidFromJson(final dynamic raw) {
return _bytesFromJson(raw, fieldName: "txid");
}
static Uint8List _bytesFromJson(final dynamic raw, {required final String fieldName}) {
if (raw == null) {
return Uint8List(0);
}
if (raw is Uint8List) {
return raw;
}
if (raw is List) {
return Uint8List.fromList(List<int>.from(raw));
}
if (raw is String) {
return base64.decode(raw);
}
throw FormatException("Invalid $fieldName in ZkoolTx json: $raw");
}
static List<T> _listFromJson<T>(final dynamic raw, final T Function(Map<String, dynamic>) parse) {
if (raw is! List) {
return const [];
}
return raw.map((final entry) => parse(Map<String, dynamic>.from(entry as Map))).toList();
}
static ZkoolTx fromJson(final Map<String, dynamic> json) {
final txJson = Map<String, dynamic>.from((json["tx"] as Map?)?.cast<String, dynamic>() ?? json);
final txAccountJson = Map<String, dynamic>.from(
(json["txaccount"] as Map?)?.cast<String, dynamic>() ?? json,
);
return ZkoolTx(
zkool_account.Tx(
id: _asInt(txJson["id"]),
txid: _txidFromJson(txJson["txid"]),
height: _asInt(txJson["height"]),
time: _asInt(txJson["time"]),
value: _asInt(txJson["value"]),
tpe: txJson["tpe"] == null ? null : _asInt(txJson["tpe"]),
zsaValue: _asInt(txJson["zsaValue"]),
assetDisplay: txJson["assetDisplay"] as String? ?? "",
isUserMemo: txJson["isUserMemo"] as bool? ?? false,
),
zkool_account.TxAccount(
id: _asInt(txAccountJson["id"]),
account: _asInt(txAccountJson["account"]),
txid: _txidFromJson(txAccountJson["txid"]),
height: _asInt(txAccountJson["height"]),
time: _asInt(txAccountJson["time"]),
notes: _listFromJson(
txAccountJson["notes"],
(final a) => zkool_account.TxNote(
id: _asInt(a["id"]),
pool: _asInt(a["pool"]),
height: _asInt(a["height"]),
tx: _asInt(a["tx"]),
value: BigInt.parse(a["value"].toString()),
scope: _asInt(a["scope"]),
locked: a["locked"] as bool? ?? false,
assetDisplay: a["assetDisplay"] as String? ?? "",
),
),
spends: _listFromJson(
txAccountJson["spends"],
(final a) => zkool_account.TxSpend(
id: _asInt(a["id"]),
pool: _asInt(a["pool"]),
height: _asInt(a["height"]),
value: BigInt.parse(a["value"].toString()),
assetDisplay: a["assetDisplay"] as String? ?? "",
),
),
outputs: _listFromJson(
txAccountJson["outputs"],
(final a) => zkool_account.TxOutput(
id: _asInt(a["id"]),
pool: _asInt(a["pool"]),
height: _asInt(a["height"]),
value: BigInt.parse(a["value"].toString()),
address: a["address"] as String? ?? "",
),
),
memos: _listFromJson(
txAccountJson["memos"],
(final a) => zkool_account.TxMemo(
note: a["note"] == null ? null : _asInt(a["note"]),
pool: _asInt(a["pool"]),
output: a["output"] == null ? null : _asInt(a["output"]),
memo: a["memo"] as String?,
memoBytes: _bytesFromJson(a["memoBytes"], fieldName: "memoBytes"),
),
),
),
);
}
}
+4 -4
View File
@@ -9,8 +9,6 @@ environment:
dependencies:
flutter:
sdk: flutter
warp_api:
path: ../scripts/zcash_lib/packages/warp_api_ffi
cw_core:
path: ../cw_core
mobx: ^2.6.0
@@ -20,11 +18,13 @@ dependencies:
path: any
flat_buffers: any
zkool:
# path: ../../zkool2
git:
url: https://github.com/cake-tech/zkool2
ref: b1642f260da4fa00dd2f8d4dd689073d0b56960a
url: https://github.com/cake-tech/zkool2.git
ref: 6329d4aacb4568ee80ff84baba5ea93b07ae1013
path_provider: any
sqflite_common_ffi: any
dependency_overrides:
decimal: ^2.3.3
+106 -6
View File
@@ -1,12 +1,112 @@
# Security Policy
## Reporting a Vulnerability
Cake Wallet is developed by **Cake Labs LLC**. We take the security and privacy of
our users seriously and welcome reports from security researchers.
If you need to report a vulnerability, please either:
> **Cake Wallet is not affiliated with CAKE.com, Clockify, or security.cake.com.**
> Those services belong to unrelated companies. The only official vulnerability
> disclosure channels for Cake Wallet are the ones listed on this page and at
> https://cakewallet.com/security.
* Open a security advisory: https://github.com/cake-tech/cake_wallet/security/advisories/new
* Send an email to `dev@cakewallet.com` with details on the vulnerability
## Reporting a vulnerability
## Supported Versions
**Please do not open a public issue, pull request, or social-media post for a
security vulnerability.** Public disclosure before a fix is available puts users'
funds and privacy at risk. Use one of the private channels below and we will
coordinate a fix and disclosure with you.
As we don't maintain previous versions of the app, only the latest release for each platform is supported and any updates will bump the version number.
1. **GitHub private security advisory (preferred).**
[Report a vulnerability](https://github.com/cake-tech/cake_wallet/security/advisories/new).
This gives you a private, structured thread with the maintainers and is the
fastest way to reach us.
2. **Encrypted email.** Send details to **security@cakewallet.com**. For sensitive
reports, please encrypt with our PGP key:
- Key: https://cakewallet.com/.well-known/cakewallet-security.asc
- Fingerprint: `DC91 6520 0271 AC6A 0533 3D3C BFE7 D9A5 0E4D 3A0A`
Both channels are monitored and automatically raise an alert in our internal
security channel, so reports will not be missed.
### What to include
- A clear description of the issue and its security impact.
- A working proof of concept is required. It must run directly against Cake
Wallet itself, using a release build or locally built version, and demonstrate
the reported behavior and security impact. Standalone Python code,
mathematical examples, or simulations that only reproduce the theory without
exercising Cake Wallet do not satisfy this requirement.
- Step-by-step reproduction instructions.
- Affected platforms (iOS, Android, macOS, Linux, Windows) and app version.
- Affected wallet types / chains, if applicable.
- Any relevant logs, addresses, or transaction IDs (for on-chain issues).
If you used AI tooling to find or write up the report, please say so.
### Communication expectations
AI should not be used to generate comments when communicating with maintainers
and other contributors. Comments are expected to be written by humans. Comments
that are believed to be written by AI may be moderated.
## Our commitment (safe harbor)
We consider security research conducted in good faith under this policy to be
authorized. We will not pursue or support legal action against researchers who:
- make a good-faith effort to avoid privacy violations, data destruction, and
interruption or degradation of our services;
- only interact with accounts they own or have explicit permission to access; and
- give us a reasonable opportunity to fix an issue before disclosing it publicly.
If in doubt about whether an action is authorized, ask us first at
security@cakewallet.com.
## What to expect
- **Acknowledgement:** within **2 business days**.
- **Triage and initial assessment:** within **7 business days**.
- **Coordinated disclosure:** we aim to ship a fix and coordinate public
disclosure within **90 days** of the report. We will keep you updated on
progress and agree on a disclosure date with you.
- **Credit:** with your permission, we are happy to publicly credit you for the
report once a fix is released.
## Scope
**In scope:** the Cake Wallet and Monero.com applications and the code in this
repository and its sibling `cake-tech` repositories — anything that could lead to
loss of funds, exposure of keys or seeds, a privacy leak, or a
failed/incorrect transaction.
**Out of scope:** issues in third-party services, exchange/swap providers, or
nodes we do not operate; reports generated solely by automated scanners without a
demonstrated impact; low-severity or informational issues on our marketing and
landing websites (for example reflected or self-XSS, missing security headers,
clickjacking on pages with no sensitive actions, or SPF/DMARC and cookie-flag
nitpicks) that do not affect the app or user funds; and social-engineering or
physical attacks.
## Rewards
At our **sole discretion**, we may offer a reward for a valid report. To be
eligible, a report must:
- be submitted **privately** through one of the channels above (a GitHub private
security advisory or `security@cakewallet.com`) — anything disclosed publicly or
sent through other channels is not eligible; and
- identify a genuine vulnerability with real impact on users — typically loss of
funds, exposure of keys or seeds, a privacy leak, or a failed or incorrect
transaction.
Trivial or low-impact findings are **not** eligible — for example, reflected XSS
or other low-severity issues on our marketing websites, missing security headers,
hardening or best-practice suggestions, automated-scanner output without a working
proof of concept, or already-known issues. There is no fixed bounty and no
guaranteed payout; whether a report qualifies, and any amount, are determined
solely by Cake Labs LLC.
## Supported versions
We do not maintain previous releases. Only the **latest release for each platform**
is supported; security fixes are delivered in new versions. Please keep Cake Wallet
up to date.
+1 -1
View File
@@ -43,7 +43,7 @@ post_install do |installer|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
+29 -35
View File
@@ -149,8 +149,6 @@ PODS:
- Flutter
- wakelock_plus (0.0.1):
- Flutter
- warp_api (0.0.1):
- Flutter
- YttriumWrapper (0.8.35)
DEPENDENCIES:
@@ -196,7 +194,6 @@ 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:
@@ -292,61 +289,58 @@ 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: 9505732798041c413152669751beeaecc5fe400f
breez_sdk_spark_flutter: ed3b6709b8ce9b40309d8728640eade5b19e8a0d
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90
cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a
cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3
device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926
cw_decred: a02cf30175a46971c1e2fa22c48407534541edc6
cw_mweb: 3aea2fb35b2bd04d8b2d21b83216f3b8fb768d85
device_display_brightness: 04374ebd653619292c1d996f00f42877ea19f17f
device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89
devicelocale: bd64aa714485a8afdaded0892c1e7d5b7f680cf8
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
dnssec_proof: d461cac7bd3301eb7447f87936745a0c1ae0a67e
fast_scanner: 44c00940355a51258cd6c2085734193cd23d95bc
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
fast_scanner: 2cb1ad3e69e645e9980fb4961396ce5804caa3e3
file_picker: 9b3292d7c8bc68c8a7bf8eb78f730e49c8efc517
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_local_notifications: ff50f8405aaa0ccdc7dcfb9022ca192e8ad9688f
flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
in_app_review: 436034b18594851a7328d7f1c2ed5ec235b79cfc
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_local_authentication: 989278c681612f1ee0e36019e149137f114b9d7f
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 2c2ff13db9e0a5647389bff88b0ecac56e3f3418
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
payjoin_flutter: d9d4c8aa16bd5dfedb9b21d0edc8199e0187d96e
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
quick_actions_ios: 4b07fb49d8d8f3518d7565fbb7a91014067a7d82
quick_actions_ios: 500fcc11711d9f646739093395c4ae8eec25f779
reown_yttrium: cee334ade64725b1d83f7b34c706a6aae2696d58
rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
rlz: 3f4022361f540a5c26d9d2474fea1d2455b69d8d
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sensitive_clipboard: 161e9abc3d56b3131309d8a321eb4690a803c16b
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sp_scanner: b1bc9321690980bdb44bba7ec85d5543e716d1b5
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
torch_dart: f4620705d10f05492fab047f2fa1c3a600e7d17d
uni_links: ed8c961e47ed9ce42b6d91e1de8049e38a4b3152
universal_ble: 45519b2aeafe62761e2c6309f8927edb5288b914
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
universal_ble: a322ebabee64f0ec27a313e89a5dd6967f37a60f
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
warp_api: 997d8fb3e777669bc6260d0c12e56cd40b9db1d1
YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966
PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a
PODFILE CHECKSUM: b08d7f4c8d9c64f5648599a30caea878b0f7616a
COCOAPODS: 1.16.2
+6 -10
View File
@@ -26,7 +26,6 @@
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, ); }; };
@@ -48,7 +47,6 @@
CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */,
CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */,
CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */,
CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -89,7 +87,6 @@
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>"; };
@@ -170,7 +167,6 @@
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */,
0C44A7182518EF4A00B570ED /* CakeWallet */,
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
@@ -483,7 +479,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -513,7 +509,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -582,7 +578,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -632,7 +628,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -664,7 +660,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -708,7 +704,7 @@
"$(PROJECT_DIR)",
);
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
+10 -18
View File
@@ -687,28 +687,20 @@ class CWBitcoin extends Bitcoin {
}
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
if (pendingTransaction is PendingLightningTransaction) return outputs;
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
if (!pendingTx.hasSilentPayment) {
if (pendingTransaction is! PendingBitcoinTransaction || !pendingTransaction.hasSilentPayment) {
return outputs;
}
final updatedOutputs = outputs.map((output) {
try {
final pendingOut = pendingTx.outputs[outputs.indexOf(output)];
final updatedOutput = output;
final stealthAddresses = pendingTransaction.stealthAddresses;
if (stealthAddresses.length != outputs.length) {
printV("well, that shouldn't happen");
return outputs;
}
updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey)
.toAddress(BitcoinNetwork.mainnet);
return updatedOutput;
} catch (_) {}
return output;
}).toList();
return updatedOutputs;
for (var i = 0; i < outputs.length; i++) {
outputs[i].stealthAddress = stealthAddresses[i];
}
return outputs;
}
@override
+17 -8
View File
@@ -336,14 +336,23 @@ class DFXBuyProvider extends BuyProvider {
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
}));
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
},
isReconnect: false,
),
);
// Recheck to handle tap-backs
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
return;
}
} else {
hardwareWalletVM!.initWallet(wallet);
}
+15 -8
View File
@@ -174,14 +174,21 @@ class RobinhoodBuyProvider extends BuyProvider {
String? countryCode}) async {
if (wallet.isHardwareWallet) {
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
await Navigator.of(context).pushNamed(Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
}));
await Navigator.of(context).pushNamed(
Routes.connectDevices,
arguments: ConnectDevicePageParams(
walletType: wallet.walletInfo.type,
hardwareWalletType: wallet.walletInfo.hardwareWalletType!,
onConnectDevice: (context, hwwVM) {
hwwVM.initWallet(wallet);
Navigator.of(context).pop();
},
isReconnect: false,
),
);
if (!hardwareWalletVM!.isConnected(wallet.walletInfo.type)) {
return;
}
} else {
hardwareWalletVM!.initWallet(wallet);
}
@@ -15,7 +15,7 @@ class RoundedOverlayCards extends StatelessWidget {
return ClipRRect(
borderRadius:
BorderRadius.only(bottomLeft: Radius.circular(25.0), bottomRight: Radius.circular(25.0)),
BorderRadius.only(bottomLeft: Radius.circular(25), bottomRight: Radius.circular(25)),
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(
@@ -30,8 +30,8 @@ class RoundedOverlayCards extends StatelessWidget {
Flexible(
child: ClipRRect(
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(25.0),
bottomRight: Radius.circular(25.0),
bottomLeft: Radius.circular(25),
bottomRight: Radius.circular(25),
),
child: Container(
decoration: BoxDecoration(
@@ -20,7 +20,7 @@ class YatAddressProvider extends AddressLookupProvider {
bool canHandle(String q) => q.hasOnlyEmojis; // Yat handle example: 🐶🐾
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsWellKnown;
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsYatService;
@override
Future<List<ParsedAddress>> resolve({
@@ -15,7 +15,7 @@ class ZcashMeAddressProvider extends AddressLookupProvider {
List<CryptoCurrency> get supportedCurrencies => [CryptoCurrency.zec];
@override
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashNames;
bool isEnabled(SettingsStore settingsStore) => settingsStore.lookupsZcashAddress;
@override
bool canHandle(String query) {
+11 -4
View File
@@ -3,6 +3,7 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/validator.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/zano/zano.dart';
import 'package:cake_wallet/zcash/zcash_network_type.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
@@ -10,8 +11,11 @@ const BEFORE_REGEX = '(^|\\s)';
const AFTER_REGEX = '(\$|\\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type, bool isTestnet = false})
: super(
AddressValidator({
required CryptoCurrency type,
bool isTestnet = false,
String? network,
}) : super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: [CryptoCurrency.btc, CryptoCurrency.ltc].contains(type)
? (String txt) {
@@ -32,7 +36,7 @@ class AddressValidator extends TextValidator {
: type == CryptoCurrency.zano
? zano?.validateAddress
: null,
pattern: getPattern(type, isTestnet: isTestnet),
pattern: getPattern(type, isTestnet: isTestnet, network: network),
length: getLength(type),
);
@@ -61,7 +65,7 @@ class AddressValidator extends TextValidator {
CryptoCurrency.btcln,
];
static String getPattern(CryptoCurrency type, {bool isTestnet = false}) {
static String? getPattern(CryptoCurrency type, {bool isTestnet = false, String? network}) {
var pattern = "";
if (type is Erc20Token) {
pattern = '0x[0-9a-zA-Z]+';
@@ -170,6 +174,9 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.hbar:
pattern = '[0-9a-zA-Z.]+';
case CryptoCurrency.zec:
if (ZcashNetworkType.isDevNetwork(network)) {
return null;
}
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
+5 -1
View File
@@ -210,7 +210,11 @@ class BackgroundSync {
}
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
tx.date.add(Duration(minutes: 1)).toIso8601String());
final action = tx.direction == TransactionDirection.incoming ? "Received" : "Sent";
final action = tx.additionalInfo['isAutoShield'] == true
? "Autoshield"
: tx.direction == TransactionDirection.incoming
? "Received"
: "Sent";
if (sharedPreferences.getBool(PreferencesKey.backgroundSyncNotificationsEnabled) ?? false) {
await showNotification(
"$action ${wallet.currency.fullName} in ${wallet.name}", "${tx.amount.toString()}");
+21 -15
View File
@@ -1,6 +1,6 @@
import 'dart:convert';
import "dart:convert";
import 'package:cw_core/utils/proxy_wrapper.dart';
import "package:cw_core/utils/proxy_wrapper.dart";
Future<String?> getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async {
try {
@@ -31,28 +31,34 @@ Uri getURlOfLightningAddress(String lightningAddress) {
}
class _LNURLPResponseDTO {
const _LNURLPResponseDTO(
this.callback,
this.maxSendable,
this.minSendable,
this.tag,
this.metadata,
);
factory _LNURLPResponseDTO.fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String,
);
final String callback;
final int maxSendable;
final int minSendable;
final String tag;
final String metadata;
const _LNURLPResponseDTO(
this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata);
static _LNURLPResponseDTO fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String);
}
class _LNURLPCallbackResponseDTO {
final String pr;
const _LNURLPCallbackResponseDTO(this.pr);
static _LNURLPCallbackResponseDTO fromJson(Map<String, dynamic> map) =>
factory _LNURLPCallbackResponseDTO.fromJson(Map<String, dynamic> map) =>
_LNURLPCallbackResponseDTO(map["pr"] as String);
final String pr;
}
+3 -1
View File
@@ -436,7 +436,9 @@ Future<void> setup({
() => TrezorConnect("cakewallet://trezor_connect", appName: "Cake Wallet"),
);
getIt.registerLazySingleton(() => TrezorConnectViewModel(getIt<TrezorConnect>()));
getIt.registerLazySingleton(
() => TrezorConnectViewModel(getIt<TrezorConnect>(), getIt<SecureStorage>()),
);
getIt.registerFactory<KeyService>(() => KeyService(getIt.get<SecureStorage>()));
+72 -4
View File
@@ -10,7 +10,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/fs_migration.dart';
import 'package:cake_wallet/entities/haven_seed_store.dart';
import 'package:cake_wallet/entities/node_list.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/entities/secret_store_key.dart';
import 'package:cake_wallet/monero/monero.dart';
@@ -46,7 +45,7 @@ const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002';
const nanoDefaultNodeUri = 'rpc.nano.to';
const nanoDefaultPowNodeUri = 'rpc.nano.to';
const solanaDefaultNodeUri = 'solana-mainnet.core.chainstack.com';
const tronDefaultNodeUri = 'api.trongrid.io';
const tronDefaultNodeUri = 'trx.nownodes.io';
const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002';
const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568';
const zanoDefaultNodeUri = '37.27.100.59:10500';
@@ -56,7 +55,7 @@ const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022';
const baseDefaultNodeUri = 'base-rpc.publicnode.com';
const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io';
const bscDefaultNodeUri = 'bsc-dataseed.bnbchain.org';
const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443';
const zcashDefaultNodeUri = 'zec.rocks:443';
Future<void> defaultSettingsMigration(
{required int version,
@@ -607,6 +606,38 @@ Future<void> defaultSettingsMigration(
enabled: true,
);
break;
case 68:
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.tron,
newDefaultUri: tronDefaultNodeUri,
currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey,
useSSL: true,
oldUri: [
'tron-rpc.publicnode.com:443',
'api.trongrid.io',
],
);
await _changeDefaultNode(
sharedPreferences: sharedPreferences,
type: WalletType.monero,
newDefaultUri: newCakeWalletMoneroUri,
currentNodePreferenceKey: PreferencesKey.currentNodeIdKey,
useSSL: true,
trusted: true,
oldUri: ['nodes.hashvault.pro:18081'],
);
break;
case 69:
_changeExchangeProviderAvailability(
sharedPreferences,
providerName: "Exolix",
enabled: false,
);
break;
case 70:
await _addTbbTokenToExistingSolanaWallets();
break;
default:
break;
}
@@ -663,7 +694,7 @@ Future<void> _changeDefaultNode({
} else {
final currentNode = nodes.firstWhereOrNull((node) => node.id == currentNodeId);
shouldReplace =
currentNode == null || (oldUri?.any((e) => currentNode!.uriRaw.contains(e)) ?? true);
currentNode == null || (oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true);
}
if (shouldReplace) {
@@ -1297,3 +1328,40 @@ Future<void> _addXaut0TokenToExistingSolanaWallets() async {
printV('Error in XAUT0 migration: $e');
}
}
Future<void> _addTbbTokenToExistingSolanaWallets() async {
try {
final tbbToken = SPLToken(
name: "The Bitcoin Bull",
symbol: "TBB",
mintAddress: "42cXQvAAr7hcPBPWAS4ocVtDyeJ4Fa6gRR2uG4gppump",
decimal: 6,
mint: "tbb",
enabled: false,
iconPath: "assets/images/tbb_icon.png",
);
final allWallets = await WalletInfo.getAll();
final solanaWallets = allWallets.where((wallet) => wallet.type == WalletType.solana).toList();
for (final walletInfo in solanaWallets) {
final sanitizedName = walletInfo.name.replaceAll(" ", "_");
final boxName = "${sanitizedName}_${SPLToken.boxName}";
Box<SPLToken> tokenBox;
if (CakeHive.isBoxOpen(boxName)) {
tokenBox = CakeHive.box<SPLToken>(boxName);
} else {
tokenBox = await CakeHive.openBox<SPLToken>(boxName);
}
final tbbAddress = tbbToken.mintAddress;
if (!tokenBox.containsKey(tbbAddress)) {
await tokenBox.put(tbbAddress, tbbToken);
}
}
} catch (e) {
printV("Error in TBB migration: $e");
}
}
+1
View File
@@ -145,4 +145,5 @@ class PreferencesKey {
static const backgroundImage = 'background_image';
static const mwebAdDismissed = "mweb_ad_dismissed";
static const balanceHideCounter = "balance_hide_counter";
static const zcashMigrationModalViewed = "zcash_migration_modal_viewed";
}
+1 -1
View File
@@ -69,7 +69,7 @@ class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonText: S.of(context).ok,
buttonAction: () {
Navigator.of(context).pop();
},
@@ -325,6 +325,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
return 'btc';
case CryptoCurrency.arb:
return 'arbitrum';
case CryptoCurrency.nano:
return 'nano';
default:
return currency.tag != null ? _normalizeTag(currency.tag!) : currency.title.toLowerCase();
}
@@ -337,6 +339,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
switch (currency) {
case CryptoCurrency.zec:
return 'zec';
case CryptoCurrency.maticpoly:
return 'matic';
default:
return currency.title.toLowerCase();
}
@@ -344,7 +348,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
String _normalizeTag(String tag) {
switch (tag) {
case 'POLY':
case 'POL':
return 'matic';
case 'LN':
return 'lightning';
@@ -539,7 +539,7 @@ class TrocadorExchangeProvider extends ExchangeProvider {
}
Future<Uri> _getUri(String path, Map<String, String> queryParams) async {
final uri = Uri.http(onionApiAuthority, path, queryParams);
final uri = Uri.https(onionApiAuthority, path, queryParams);
if (useTorOnly) return uri;
+5 -3
View File
@@ -23,6 +23,7 @@ import 'package:cake_wallet/exchange/exchange_template.dart';
import 'package:cake_wallet/exchange/trade_legacy.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/locales/locale.dart';
import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
import 'package:cake_wallet/order/order.dart';
import 'package:cake_wallet/reactions/bootstrap.dart';
import 'package:cake_wallet/router.dart' as Router;
@@ -315,7 +316,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 67,
initialMigrationVersion: 70,
);
}
@@ -425,8 +426,9 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
navigatorKey: navigatorKey,
debugShowCheckedModeBanner: false,
builder: (context, child) => MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: child!),
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling),
child: MoneySettingsProvider(settingsStore: settingsStore, child: child!),
),
theme: theme,
darkTheme: darkTheme,
themeMode: themeMode,
+8
View File
@@ -236,12 +236,14 @@ class CWMonero extends Monero {
required String password,
required int height,
required HardwareWalletService hardwareWalletService,
required String? passphrase,
}) =>
MoneroRestoreWalletFromHardwareCredentials(
name: name,
password: password,
height: height,
hardwareWalletService: hardwareWalletService,
passphrase: passphrase,
);
@override
@@ -418,6 +420,12 @@ class CWMonero extends Monero {
return moneroWallet.needExportOutputs(amount);
}
@override
bool hasUnknownKeyImages(Object wallet) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.hasUnknownKeyImages();
}
@override
void monerocCheck() => checkIfMoneroCIsFine();
+12 -8
View File
@@ -90,14 +90,18 @@ class _NewDashboardState extends State<NewDashboard> {
),
),
),
SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
// Invisible pointer-absorbing strip behind the nav bar: it must not
// be reachable by screen-reader traversal either.
ExcludeSemantics(
child: SafeArea(
bottom: !(Platform.isIOS),
child: SizedBox(
width: double.infinity,
height: NewMainNavBar.barHeight + NewMainNavBar.barBottomPadding,
child: AbsorbPointer(
absorbing: true,
child: Container(color: Colors.transparent),
),
),
),
),
+1
View File
@@ -56,6 +56,7 @@ class _AboutPageState extends State<AboutPage> {
topBar: ModalTopBar(
title: S.of(context).about,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
),
content: Container(
+70 -44
View File
@@ -128,8 +128,10 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
ModalTopBar(
title: S.of(context).wallet_accounts,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
onLeadingPressed: Navigator.of(context).maybePop,
trailingIcon: Icon(Icons.refresh),
trailingSemanticLabel: S.of(context).reset,
onTrailingPressed: showResetDialog,
),
Padding(
@@ -172,17 +174,29 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
final card = _items[index].card;
// The stack is ordered bottom to top, so the last item
// is the account currently in front — the selected one.
final selectedItemIndex = _items.length - 1;
return Container(
key: ValueKey(index),
child: GestureDetector(
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
// One labeled, selectable node per account; the card's own
// texts stay reachable underneath it.
child: Semantics(
button: true,
selected: selectedItemIndex == index,
label: _items[index].accountListItem.label,
onTap: () => reorder(index, _items.length),
child: GestureDetector(
excludeFromSemantics: true,
onTap: () {
reorder(index, _items.length);
},
child: Align(
alignment: Alignment.topCenter,
heightFactor: _kStackVisibleFactor,
child: card,
),
),
),
);
@@ -197,31 +211,36 @@ class _AccountCustomizerState extends State<AccountCustomizer> {
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
),
Text(
S.of(context).add_account,
style: TextStyle(
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
borderRadius: BorderRadius.circular(999999),
onTap: _showAddAccountModal,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(999999)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 8,
children: [
Icon(
Icons.add,
size: 28,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
Text(
S.of(context).add_account,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500),
)
],
),
),
),
),
),
@@ -404,6 +423,8 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
final TextEditingController _controller = TextEditingController();
bool _loading = false;
Future<void> _generateAccountName() async => _controller.text = await generateName();
@override
Widget build(BuildContext context) {
return Container(
@@ -439,18 +460,23 @@ class _AccountCreationModalState extends State<AccountCreationModal> {
),
Padding(
padding: const EdgeInsets.all(12.0),
child: GestureDetector(
onTap: () async {
_controller.text = await generateName();
},
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
child: Semantics(
button: true,
label: S.of(context).generate_name,
onTap: _generateAccountName,
child: ExcludeSemantics(
child: GestureDetector(
onTap: _generateAccountName,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: BorderRadius.circular(5)),
child: CakeImageWidget(
imageUrl: "assets/new-ui/randomize.svg",
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
),
),
),
),
+178 -138
View File
@@ -20,6 +20,7 @@ import 'package:cake_wallet/wownero/wownero.dart';
import 'package:cw_core/card_design.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/utils/list_item.dart';
@@ -105,6 +106,7 @@ class _NewAddressesPageState extends State<NewAddressesPage> {
ModalTopBar(
title: widget.showHidden ? S.of(context).hidden_addresses : S.of(context).addresses,
leadingIcon: Icon(Icons.arrow_back),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: Navigator.of(context).pop,
onTrailingPressed: () {}),
Expanded(
@@ -212,24 +214,31 @@ class AddressSearchBox extends StatelessWidget {
border: Border.all(
color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1),
borderRadius: BorderRadius.circular(99999)),
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: S.of(context).search,
hintStyle: TextStyle(
fontWeight: FontWeight.w600,
// The hint names the field only while it is empty, so the label is
// supplied once there is text to keep exactly one announcement.
child: MergeSemantics(
child: Semantics(
label: controller.text.isEmpty ? null : S.of(context).search,
child: TextField(
controller: controller,
decoration: InputDecoration(
hintText: S.of(context).search,
hintStyle: TextStyle(
fontWeight: FontWeight.w600,
),
prefixIcon: ExcludeSemantics(child: Icon(Icons.search)),
filled: false,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
),
prefixIcon: Icon(Icons.search),
filled: false,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(99999),
borderSide: BorderSide.none,
),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
),
),
@@ -333,116 +342,139 @@ class AddressRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasLabel = item.name != null && item.name!.isNotEmpty;
final hideLabel = item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address;
return GestureDetector(
onTap: onSelect,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LongPressPopupBuilder(
popup: LongPressMenu(
items: [
LongPressMenuItem(
label: S.of(context).set_label,
iconPath: "assets/new-ui/address_set_label.svg",
onSelected: () async {
Navigator.of(context, rootNavigator: true).pop();
final res = await showPopUp(
context: context,
builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
Future<void> editLabel() async {
final res = await showPopUp(
context: context, builder: (context) => getIt.get<AddressLabelInputPopup>(param1: item));
if (res != null) {
onLabelChanged();
}
}),
LongPressMenuItem(
label: item.isHidden ? S.of(context).unhide_address : S.of(context).hide_address,
iconPath: "assets/new-ui/address_hide.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
onAddressHidden();
},
color: Theme.of(context).colorScheme.error),
LongPressMenuItem(
label: 'Info',
iconPath: "assets/images/info_icon.svg",
onSelected: () async {
Navigator.of(context, rootNavigator: true).pop();
await showPopUp(
context: context,
builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
}),
],
),
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(first ? 16 : 0),
bottom: Radius.circular(last ? 16 : 0),
if (res != null) {
onLabelChanged();
}
}
Future<void> showInfo() async {
await showPopUp(
context: context, builder: (context) => getIt.get<AddressInfoPopup>(param1: item));
}
// The row is a single control: selection on tap, and the actions that are
// otherwise reachable only by long press exposed as custom actions.
return MergeSemantics(
child: Semantics(
button: true,
selected: selected,
customSemanticsActions: <CustomSemanticsAction, VoidCallback>{
CustomSemanticsAction(label: S.of(context).set_label): editLabel,
CustomSemanticsAction(label: hideLabel): onAddressHidden,
CustomSemanticsAction(label: S.of(context).info): showInfo,
},
child: GestureDetector(
onTap: onSelect,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: LongPressPopupBuilder(
popup: LongPressMenu(
items: [
LongPressMenuItem(
label: S.of(context).set_label,
iconPath: "assets/new-ui/address_set_label.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
editLabel();
}),
LongPressMenuItem(
label: hideLabel,
iconPath: "assets/new-ui/address_hide.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
onAddressHidden();
},
color: Theme.of(context).colorScheme.error),
LongPressMenuItem(
label: S.of(context).info,
iconPath: "assets/images/info_icon.svg",
onSelected: () {
Navigator.of(context, rootNavigator: true).pop();
showInfo();
}),
],
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
children: [
Row(
spacing: 4,
child: AnimatedContainer(
duration: Duration(milliseconds: 150),
decoration: BoxDecoration(
color: selected
? Theme.of(context).colorScheme.surfaceContainerHigh
: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(
top: Radius.circular(first ? 16 : 0),
bottom: Radius.circular(last ? 16 : 0),
),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
spacing: 8,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
children: [
if (hasLabel)
Text(
item.name!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
),
Expanded(
child: AddressFormatter.buildSegmentedAddress(
address: item.address,
walletType: walletType,
textAlign: TextAlign.left,
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
Row(
spacing: 4,
children: [
if (hasLabel)
Text(
item.name!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: hasLabel
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
),
shouldTruncate: (hasLabel)),
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
),
Expanded(
child: AddressFormatter.buildSegmentedAddress(
address: item.address,
walletType: walletType,
textAlign: TextAlign.left,
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 12,
fontWeight: FontWeight.w400,
color: hasLabel
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.onSurface,
),
shouldTruncate: (hasLabel)),
),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${S.of(context).transactions}: ${item.txCount}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${S.of(context).transactions}: ${item.txCount}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant),
),
Text(
"${hasReceived ? S.of(context).received : S.of(context).balance}: ${item.balance}",
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.onSurfaceVariant)),
],
)
],
),
),
// The row's selected state carries this information.
if (selected)
ExcludeSemantics(
child: CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg"))
],
),
if (selected) CakeImageWidget(imageUrl: "assets/new-ui/checkmark.svg")
],
),
),
),
),
@@ -462,26 +494,34 @@ class ShowHiddenButton extends StatelessWidget {
child: Column(
children: [
Material(
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
},
child: Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).show_hidden_addresses),
RotatedBox(
quarterTurns: 1,
child: CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg"))
],
child: MergeSemantics(
child: Semantics(
button: true,
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(Routes.receiveAddresses, arguments: true);
},
child: Container(
height: 48,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(S.of(context).show_hidden_addresses),
ExcludeSemantics(
child: RotatedBox(
quarterTurns: 1,
child:
CakeImageWidget(imageUrl: "assets/new-ui/dropdown_arrow.svg")),
)
],
),
),
),
),
),
@@ -116,8 +116,10 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
ModalTopBar(
title: S.of(context).enter_amount,
leadingIcon: Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context, rootNavigator: true).pop(),
trailingIcon: Icon(Icons.history),
trailingSemanticLabel: S.of(context).history,
onTrailingPressed: () {
Navigator.pushNamed(context, Routes.bridgeHistoryPage,
arguments: widget.bridgeHistoryViewModel);
@@ -274,6 +276,7 @@ class _BridgeAmountPageState extends State<BridgeAmountPage> {
backgroundColor: theme.colorScheme.primary,
iconColor: theme.colorScheme.onPrimary,
icon: Icon(Icons.arrow_forward, size: 20),
semanticLabel: S.of(context).continue_text,
onPressed: () {
Navigator.pushNamed(
context,
@@ -87,6 +87,7 @@ class _BridgeConfirmSheetState extends State<BridgeConfirmSheet> {
],
),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
);
},
@@ -46,6 +46,7 @@ class BridgeDetailPage extends StatelessWidget {
ModalTopBar(
title: "Bridge Transfer Details",
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
Expanded(
@@ -1,4 +1,5 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/view_model/bridge/bridge_history_view_model.dart';
@@ -25,6 +26,7 @@ class BridgeHistoryPage extends StatelessWidget {
ModalTopBar(
title: "Bridge history",
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
Expanded(
@@ -1,4 +1,5 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
import 'package:cake_wallet/routes.dart';
@@ -30,6 +31,7 @@ class BridgeNetworkPage extends StatelessWidget {
ModalTopBar(
title: 'Destination Network',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
SizedBox(height: 48),
@@ -55,6 +55,7 @@ class _BridgeReceiveAddressInputPageState extends State<BridgeReceiveAddressInpu
ModalTopBar(
title: 'Input Receive Address',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
SizedBox(height: 48),
@@ -1,4 +1,5 @@
import 'package:cake_wallet/view_model/bridge/bridge_receiving_wallet_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_confirm_sheet.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receive_address_input_page.dart';
import 'package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
@@ -42,6 +43,7 @@ class _BridgeReceivingWalletPageState extends State<BridgeReceivingWalletPage> {
ModalTopBar(
title: 'Receiving Wallet',
leadingIcon: const Icon(Icons.arrow_back_ios_new, size: 18),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).pop(),
),
const SizedBox(height: 48),
+1
View File
@@ -84,6 +84,7 @@ class _CardCustomizerState extends State<CardCustomizer> {
ModalTopBar(
title: editEnabled ? S.of(context).edit_account : S.of(context).edit_card,
leadingIcon: Icon(Icons.close),
leadingSemanticLabel: S.of(context).close,
// trailingIcon: editEnabled ? Icon(Icons.delete_forever) : null,
onLeadingPressed: () => Navigator.of(context).maybePop(),
// onTrailingPressed: () {},
+22
View File
@@ -13,10 +13,14 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/mweb_ad.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/top_bar.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/unconfirmed_balance_widget.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/wallet_info.dart';
import "package:cake_wallet/new-ui/widgets/coins_page/zcash_migration_modal.dart";
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/nft_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/transaction_direction.dart";
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -48,6 +52,24 @@ class _NewHomePageState extends State<NewHomePage> {
_lightningMode = false;
});
});
reaction((_) => widget.dashboardViewModel.isMigratingToIronwood, (val) {
if (val && !widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed) {
if(!context.mounted) {
return;
}
widget.dashboardViewModel.settingsStore.zcashMigrationModalViewed = true;
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => ZcashMigrationModal(
hasContinue: true,
balance: widget.dashboardViewModel.wallet.balance.values.first.unavailable
.toStringWithSymbol(),
),
);
}
});
}
void _setAccountViewModel() {

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