mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -226,4 +226,5 @@ scripts/android/yttrium
|
||||
scripts/bitbox_flutter
|
||||
scripts/reown_flutter
|
||||
|
||||
*.salive
|
||||
*.salive
|
||||
android/.kotlin
|
||||
|
||||
@@ -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,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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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})));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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>);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -902,6 +902,9 @@ abstract class SolanaWalletBase
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> isTokenVerifiedOnJupiter(String mintAddress) =>
|
||||
_client.isTokenVerifiedOnJupiter(mintAddress);
|
||||
|
||||
void _setTransactionUpdateTimer() {
|
||||
if (_transactionsUpdateTimer?.isActive ?? false) {
|
||||
_transactionsUpdateTimer!.cancel();
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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.';
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1376
-791
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}'
|
||||
|
||||
@@ -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()}");
|
||||
|
||||
@@ -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
@@ -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>()));
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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: () {},
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user