feat: rebase btc-addr-types, migrate to bitcoin_base

This commit is contained in:
Rafael Saes 2024-02-26 15:32:54 -03:00
parent 5b1f17c1fb
commit 6b795b5ba3
29 changed files with 1046 additions and 223 deletions

View file

@ -16,6 +16,7 @@ class BitcoinAddressRecord {
required this.type, required this.type,
String? scriptHash, String? scriptHash,
required this.network, required this.network,
this.silentPaymentTweak,
}) : _txCount = txCount, }) : _txCount = txCount,
_balance = balance, _balance = balance,
_name = name, _name = name,
@ -23,7 +24,7 @@ class BitcoinAddressRecord {
scriptHash = scriptHash =
scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null); scriptHash ?? (network != null ? sh.scriptHash(address, network: network) : null);
factory BitcoinAddressRecord.fromJSON(String jsonSource, BasedUtxoNetwork? network) { factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord( return BitcoinAddressRecord(
@ -42,6 +43,7 @@ class BitcoinAddressRecord {
network: (decoded['network'] as String?) == null network: (decoded['network'] as String?) == null
? network ? network
: BasedUtxoNetwork.fromName(decoded['network'] as String), : BasedUtxoNetwork.fromName(decoded['network'] as String),
silentPaymentTweak: decoded['silentPaymentTweak'] as String?,
); );
} }
@ -57,6 +59,7 @@ class BitcoinAddressRecord {
bool _isUsed; bool _isUsed;
String? scriptHash; String? scriptHash;
BasedUtxoNetwork? network; BasedUtxoNetwork? network;
final String? silentPaymentTweak;
int get txCount => _txCount; int get txCount => _txCount;
@ -96,5 +99,6 @@ class BitcoinAddressRecord {
'type': type.toString(), 'type': type.toString(),
'scriptHash': scriptHash, 'scriptHash': scriptHash,
'network': network?.value, 'network': network?.value,
'silentPaymentTweak': silentPaymentTweak,
}); });
} }

View file

@ -8,6 +8,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)'); static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)'); static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
const BitcoinReceivePageOption._(this.value); const BitcoinReceivePageOption._(this.value);
final String value; final String value;
@ -34,6 +36,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
return BitcoinReceivePageOption.p2pkh; return BitcoinReceivePageOption.p2pkh;
case P2shAddressType.p2wpkhInP2sh: case P2shAddressType.p2wpkhInP2sh:
return BitcoinReceivePageOption.p2sh; return BitcoinReceivePageOption.p2sh;
case SilentPaymentsAddresType.p2sp:
return BitcoinReceivePageOption.silent_payments;
case SegwitAddresType.p2wpkh: case SegwitAddresType.p2wpkh:
default: default:
return BitcoinReceivePageOption.p2wpkh; return BitcoinReceivePageOption.p2wpkh;

View file

@ -1,14 +1,38 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_core/unspent_transaction_output.dart'; import 'package:cw_core/unspent_transaction_output.dart';
class BitcoinUnspent extends Unspent { class BitcoinUnspent extends Unspent {
BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout) BitcoinUnspent(BitcoinAddressRecord addressRecord, String hash, int value, int vout,
{this.silentPaymentTweak, this.type})
: bitcoinAddressRecord = addressRecord, : bitcoinAddressRecord = addressRecord,
super(addressRecord.address, hash, value, vout, null); super(addressRecord.address, hash, value, vout, null);
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) => factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) =>
BitcoinUnspent( BitcoinUnspent(
address, json['tx_hash'] as String, json['value'] as int, json['tx_pos'] as int); address,
json['tx_hash'] as String,
json['value'] as int,
json['tx_pos'] as int,
silentPaymentTweak: json['silent_payment_tweak'] as String?,
type: json['type'] == null
? null
: BitcoinAddressType.values.firstWhere((e) => e.toString() == json['type']),
);
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'address_record': bitcoinAddressRecord.toJSON(),
'tx_hash': hash,
'value': value,
'tx_pos': vout,
'silent_payment_tweak': silentPaymentTweak,
'type': type.toString(),
};
return json;
}
final BitcoinAddressRecord bitcoinAddressRecord; final BitcoinAddressRecord bitcoinAddressRecord;
String? silentPaymentTweak;
BitcoinAddressType? type;
} }

View file

@ -30,6 +30,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
List<BitcoinAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
SilentPaymentOwner? silentAddress,
}) : super( }) : super(
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
@ -46,10 +49,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
currency: CryptoCurrency.btc) { currency: CryptoCurrency.btc) {
walletAddresses = BitcoinWalletAddresses( walletAddresses = BitcoinWalletAddresses(
walletInfo, walletInfo,
electrumClient: electrumClient,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex, initialChangeAddressIndex: initialChangeAddressIndex,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
silentAddress: silentAddress,
mainHd: hd, mainHd: hd,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"), sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
network: networkParam ?? network, network: networkParam ?? network,
@ -67,9 +72,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
String? addressPageType, String? addressPageType,
BasedUtxoNetwork? network, BasedUtxoNetwork? network,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
List<BitcoinAddressRecord>? initialSilentAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
int initialSilentAddressIndex = 0,
}) async { }) async {
return BitcoinWallet( return BitcoinWallet(
mnemonic: mnemonic, mnemonic: mnemonic,
@ -77,6 +84,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialSilentAddresses: initialSilentAddresses,
initialSilentAddressIndex: initialSilentAddressIndex,
silentAddress: await SilentPaymentOwner.fromMnemonic(mnemonic,
hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
initialBalance: initialBalance, initialBalance: initialBalance,
seedBytes: await mnemonicToSeedBytes(mnemonic), seedBytes: await mnemonicToSeedBytes(mnemonic),
initialRegularAddressIndex: initialRegularAddressIndex, initialRegularAddressIndex: initialRegularAddressIndex,
@ -101,6 +112,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp.addresses,
initialSilentAddresses: snp.silentAddresses,
initialSilentAddressIndex: snp.silentAddressIndex,
silentAddress: await SilentPaymentOwner.fromMnemonic(snp.mnemonic,
hrp: snp.network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
initialBalance: snp.balance, initialBalance: snp.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic), seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
initialRegularAddressIndex: snp.regularAddressIndex, initialRegularAddressIndex: snp.regularAddressIndex,

View file

@ -15,10 +15,12 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
required super.mainHd, required super.mainHd,
required super.sideHd, required super.sideHd,
required super.network, required super.network,
required super.electrumClient,
super.initialAddresses, super.initialAddresses,
super.initialRegularAddressIndex, super.initialRegularAddressIndex,
super.initialChangeAddressIndex, super.initialChangeAddressIndex,
super.initialSilentAddresses,
super.initialSilentAddressIndex = 0,
super.silentAddress,
}) : super(walletInfo); }) : super(walletInfo);
@override @override

View file

@ -349,6 +349,12 @@ class ElectrumClient {
return null; return null;
}); });
BehaviorSubject<Object>? chainTipUpdate() {
_id += 1;
return subscribe<Object>(
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
}
BehaviorSubject<Object>? scripthashUpdate(String scripthash) { BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
_id += 1; _id += 1;
return subscribe<Object>( return subscribe<Object>(

View file

@ -1,9 +1,8 @@
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart'; import 'package:cw_core/format_amount.dart';
@ -20,6 +19,8 @@ class ElectrumTransactionBundle {
} }
class ElectrumTransactionInfo extends TransactionInfo { class ElectrumTransactionInfo extends TransactionInfo {
BitcoinUnspent? unspent;
ElectrumTransactionInfo(this.type, ElectrumTransactionInfo(this.type,
{required String id, {required String id,
required int height, required int height,
@ -28,7 +29,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
required TransactionDirection direction, required TransactionDirection direction,
required bool isPending, required bool isPending,
required DateTime date, required DateTime date,
required int confirmations}) { required int confirmations,
String? to,
this.unspent}) {
this.id = id; this.id = id;
this.height = height; this.height = height;
this.amount = amount; this.amount = amount;
@ -37,6 +40,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.date = date; this.date = date;
this.isPending = isPending; this.isPending = isPending;
this.confirmations = confirmations; this.confirmations = confirmations;
this.to = to;
} }
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type, factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
@ -144,50 +148,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
confirmations: bundle.confirmations); confirmations: bundle.confirmations);
} }
factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex,
{List<String>? addresses, required int height, int? timestamp, required int confirmations}) {
final tx = bitcoin.Transaction.fromHex(hex);
var exist = false;
var amount = 0;
if (addresses != null) {
tx.outs.forEach((out) {
try {
final p2pkh =
bitcoin.P2PKH(data: PaymentData(output: out.script), network: bitcoin.bitcoin);
exist = addresses.contains(p2pkh.data.address);
if (exist) {
amount += out.value!;
}
} catch (_) {}
});
}
final date =
timestamp != null ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now();
return ElectrumTransactionInfo(type,
id: tx.getId(),
height: height,
isPending: false,
fee: null,
direction: TransactionDirection.incoming,
amount: amount,
date: date,
confirmations: confirmations);
}
factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) { factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
return ElectrumTransactionInfo(type, return ElectrumTransactionInfo(
id: data['id'] as String, type,
height: data['height'] as int, id: data['id'] as String,
amount: data['amount'] as int, height: data['height'] as int,
fee: data['fee'] as int, amount: data['amount'] as int,
direction: parseTransactionDirectionFromInt(data['direction'] as int), fee: data['fee'] as int,
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), direction: parseTransactionDirectionFromInt(data['direction'] as int),
isPending: data['isPending'] as bool, date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
confirmations: data['confirmations'] as int); isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int,
to: data['to'] as String?,
unspent: data['unspent'] != null
? BitcoinUnspent.fromJSON(
BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String),
data['unspent'] as Map<String, dynamic>)
: null,
);
} }
final WalletType type; final WalletType type;
@ -231,6 +209,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending; m['isPending'] = isPending;
m['confirmations'] = confirmations; m['confirmations'] = confirmations;
m['fee'] = fee; m['fee'] = fee;
m['to'] = to;
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
return m; return m;
} }
String toString() {
return 'ElectrumTransactionInfo(id: $id, height: $height, amount: $amount, fee: $fee, direction: $direction, date: $date, isPending: $isPending, confirmations: $confirmations, to: $to, unspent: $unspent)';
}
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
@ -143,21 +144,93 @@ abstract class ElectrumWalletBase
List<BitcoinUnspent> unspentCoins; List<BitcoinUnspent> unspentCoins;
List<int> _feeRates; List<int> _feeRates;
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject; Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
BehaviorSubject<Object>? _chainTipUpdateSubject;
bool _isTransactionUpdating; bool _isTransactionUpdating;
// Future<Isolate>? _isolate;
void Function(FlutterErrorDetails)? _onError; void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer;
static const int _autoSaveInterval = 30;
Future<void> init() async { Future<void> init() async {
await walletAddresses.init(); await walletAddresses.init();
await transactionHistory.init(); await transactionHistory.init();
await save();
_autoSaveTimer =
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
} }
// @action
// Future<void> _setListeners(int height, {int? chainTip}) async {
// final currentChainTip = chainTip ?? await electrumClient.getCurrentBlockChainTip() ?? 0;
// syncStatus = AttemptingSyncStatus();
// if (_isolate != null) {
// final runningIsolate = await _isolate!;
// runningIsolate.kill(priority: Isolate.immediate);
// }
// final receivePort = ReceivePort();
// _isolate = Isolate.spawn(
// startRefresh,
// ScanData(
// sendPort: receivePort.sendPort,
// primarySilentAddress: walletAddresses.primarySilentAddress!,
// networkType: networkType,
// height: height,
// chainTip: currentChainTip,
// electrumClient: ElectrumClient(),
// transactionHistoryIds: transactionHistory.transactions.keys.toList(),
// node: electrumClient.uri.toString(),
// labels: walletAddresses.labels,
// ));
// await for (var message in receivePort) {
// if (message is BitcoinUnspent) {
// if (!unspentCoins.any((utx) =>
// utx.hash.contains(message.hash) &&
// utx.vout == message.vout &&
// utx.address.contains(message.address))) {
// unspentCoins.add(message);
// if (unspentCoinsInfo.values.any((element) =>
// element.walletId.contains(id) &&
// element.hash.contains(message.hash) &&
// element.address.contains(message.address))) {
// _addCoinInfo(message);
// await walletInfo.save();
// await save();
// }
// balance[currency] = await _fetchBalances();
// }
// }
// if (message is Map<String, ElectrumTransactionInfo>) {
// transactionHistory.addMany(message);
// await transactionHistory.save();
// }
// // check if is a SyncStatus type since "is SyncStatus" doesn't work here
// if (message is SyncResponse) {
// syncStatus = message.syncStatus;
// walletInfo.restoreHeight = message.height;
// await walletInfo.save();
// }
// }
// }
@action @action
@override @override
Future<void> startSync() async { Future<void> startSync() async {
try { try {
syncStatus = AttemptingSyncStatus(); await _setInitialHeight();
} catch (_) {}
try {
rescan(height: walletInfo.restoreHeight);
await updateTransactions(); await updateTransactions();
_subscribeForUpdates(); _subscribeForUpdates();
await updateUnspent(); await updateUnspent();
@ -187,6 +260,12 @@ abstract class ElectrumWalletBase
} }
}; };
syncStatus = ConnectedSyncStatus(); syncStatus = ConnectedSyncStatus();
// final currentChainTip = await electrumClient.getCurrentBlockChainTip();
// if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
// _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
// }
} catch (e) { } catch (e) {
print(e.toString()); print(e.toString());
syncStatus = FailedSyncStatus(); syncStatus = FailedSyncStatus();
@ -213,6 +292,124 @@ abstract class ElectrumWalletBase
allInputsAmount += utx.value; allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value; leftAmount = leftAmount - utx.value;
if (utx.bitcoinAddressRecord.silentPaymentTweak != null) {
// final d = ECPrivate.fromHex(walletAddresses.primarySilentAddress!.spendPrivkey.toHex())
// .tweakAdd(utx.bitcoinAddressRecord.silentPaymentTweak!)!;
// inputPrivKeys.add(bitcoin.PrivateKeyInfo(d, true));
// address = bitcoin.P2trAddress(address: utx.address, networkType: networkType);
// keyPairs.add(bitcoin.ECPair.fromPrivateKey(d.toCompressedHex().fromHex,
// compressed: true, network: networkType));
// scriptType = bitcoin.AddressType.p2tr;
// script = bitcoin.P2trAddress(pubkey: d.publicKey.toHex(), networkType: networkType)
// .scriptPubkey
// .toBytes();
}
final address = _addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
index: utx.bitcoinAddressRecord.index,
network: network);
privateKeys.add(privkey);
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utx.hash,
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: _getScriptType(address),
),
ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
),
);
bool amountIsAcquired = !sendAll && leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
}
if (inputs.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
final allAmountFee = transactionCredentials.feeRate != null
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
final allAmount = allInputsAmount - allAmountFee;
var credentialsAmount = 0;
var amount = 0;
var fee = 0;
if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
throw BitcoinTransactionWrongBalanceException(currency);
}
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
if (allAmount - credentialsAmount < minAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = credentialsAmount;
if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
outputsCount: outputs.length + 1);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
outputsCount: outputs.length + 1);
}
} else {
final output = outputs.first;
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
if (credentialsAmount > allAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
amount = output.sendAll || allAmount - credentialsAmount < minAmount
? allAmount
: credentialsAmount;
if (output.sendAll || amount == allAmount) {
fee = allAmountFee;
} else if (transactionCredentials.feeRate != null) {
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
} else {
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
}
}
if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final totalAmount = amount + fee;
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final txb = bitcoin.TransactionBuilder(network: networkType);
final changeAddress = await walletAddresses.getChangeAddress();
var leftAmount = totalAmount;
var totalInputAmount = 0;
inputs.clear();
for (final utx in unspentCoins) {
if (utx.isSending) {
leftAmount = leftAmount - utx.value;
final address = _addressTypeFromStr(utx.address, network); final address = _addressTypeFromStr(utx.address, network);
final privkey = generateECPrivate( final privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
@ -304,23 +501,22 @@ abstract class ElectrumWalletBase
} }
} }
return EstimatedTxResult(utxos: utxos, privateKeys: privateKeys, fee: fee, amount: amount); if (SilentPaymentAddress.regex.hasMatch(outputAddress)) {
} // final outpointsHash = SilentPayment.hashOutpoints(outpoints);
// final generatedOutputs = SilentPayment.generateMultipleRecipientPubkeys(inputPrivKeys,
// outpointsHash, SilentPaymentDestination.fromAddress(outputAddress, outputAmount!));
@override // generatedOutputs.forEach((recipientSilentAddress, generatedOutput) {
Future<PendingTransaction> createTransaction(Object credentials) async { // generatedOutput.forEach((output) {
try { // outputs.add(BitcoinOutputDetails(
final outputs = <BitcoinOutput>[]; // address: P2trAddress(
final outputAddresses = <BitcoinBaseAddress>[]; // program: ECPublic.fromHex(output.$1.toHex()).toTapPoint(),
final transactionCredentials = credentials as BitcoinTransactionCredentials; // networkType: networkType),
final hasMultiDestination = transactionCredentials.outputs.length > 1; // value: BigInt.from(output.$2),
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; // ));
// });
var credentialsAmount = 0; // });
}
for (final out in transactionCredentials.outputs) {
final outputAddress = out.isParsedAddress ? out.extractedAddress! : out.address;
final address = _addressTypeFromStr(outputAddress, network);
outputAddresses.add(address); outputAddresses.add(address);
@ -392,6 +588,8 @@ abstract class ElectrumWalletBase
? SegwitAddresType.p2wpkh.toString() ? SegwitAddresType.p2wpkh.toString()
: walletInfo.addressPageType.toString(), : walletInfo.addressPageType.toString(),
'balance': balance[currency]?.toJSON(), 'balance': balance[currency]?.toJSON(),
'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(),
'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(),
'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet', 'network_type': network == BitcoinNetwork.testnet ? 'testnet' : 'mainnet',
}); });
@ -498,18 +696,31 @@ abstract class ElectrumWalletBase
} }
@override @override
Future<void> rescan({required int height}) async => throw UnimplementedError(); Future<void> rescan({required int height, int? chainTip, ScanData? scanData}) async {
// _setListeners(height);
}
@override @override
Future<void> close() async { Future<void> close() async {
try { try {
await electrumClient.close(); await electrumClient.close();
} catch (_) {} } catch (_) {}
_autoSaveTimer?.cancel();
} }
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> updateUnspent() async { Future<void> updateUnspent() async {
// Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspent != null) {
if (!unspentCoins
.any((utx) => utx.hash.contains(tx.unspent!.hash) && utx.vout == tx.unspent!.vout)) {
unspentCoins.add(tx.unspent!);
}
}
});
List<BitcoinUnspent> updatedUnspentCoins = []; List<BitcoinUnspent> updatedUnspentCoins = [];
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet(); final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
@ -538,7 +749,7 @@ abstract class ElectrumWalletBase
final coinInfoList = unspentCoinsInfo.values.where((element) => final coinInfoList = unspentCoinsInfo.values.where((element) =>
element.walletId.contains(id) && element.walletId.contains(id) &&
element.hash.contains(coin.hash) && element.hash.contains(coin.hash) &&
element.vout == coin.vout); element.address.contains(coin.address));
if (coinInfoList.isNotEmpty) { if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first; final coinInfo = coinInfoList.first;
@ -555,6 +766,7 @@ abstract class ElectrumWalletBase
await _refreshUnspentCoinsInfo(); await _refreshUnspentCoinsInfo();
} }
@action
Future<void> _addCoinInfo(BitcoinUnspent coin) async { Future<void> _addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo( final newInfo = UnspentCoinsInfo(
walletId: id, walletId: id,
@ -619,19 +831,17 @@ abstract class ElectrumWalletBase
confirmations = verboseTransaction['confirmations'] as int? ?? 0; confirmations = verboseTransaction['confirmations'] as int? ?? 0;
} }
final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex); final original = BtcTransaction.fromRaw(transactionHex);
final ins = <bitcoin_base.BtcTransaction>[]; final ins = <BtcTransaction>[];
for (final vin in original.inputs) { for (final vin in original.inputs) {
try { try {
final id = HEX.encode(HEX.decode(vin.txId).reversed.toList()); final id = HEX.encode(HEX.decode(vin.txId).reversed.toList());
final txHex = await electrumClient.getTransactionHex(hash: id); final txHex = await electrumClient.getTransactionHex(hash: id);
final tx = bitcoin_base.BtcTransaction.fromRaw(txHex); final tx = BtcTransaction.fromRaw(txHex);
ins.add(tx); ins.add(tx);
} catch (_) { } catch (_) {
ins.add(bitcoin_base.BtcTransaction.fromRaw( ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId)));
await electrumClient.getTransactionHex(hash: vin.txId),
));
} }
} }
@ -767,7 +977,7 @@ abstract class ElectrumWalletBase
} }
} }
void _subscribeForUpdates() { void _subscribeForUpdates() async {
scriptHashes.forEach((sh) async { scriptHashes.forEach((sh) async {
await _scripthashesUpdateSubject[sh]?.close(); await _scripthashesUpdateSubject[sh]?.close();
_scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh);
@ -786,6 +996,23 @@ abstract class ElectrumWalletBase
} }
}); });
}); });
await _chainTipUpdateSubject?.close();
_chainTipUpdateSubject = electrumClient.chainTipUpdate();
_chainTipUpdateSubject?.listen((_) async {
try {
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
// _setListeners(walletInfo.restoreHeight, chainTip: currentHeight);
} catch (e, s) {
print(e.toString());
_onError?.call(FlutterErrorDetails(
exception: e,
stack: s,
library: this.runtimeType.toString(),
));
}
});
} }
Future<ElectrumBalance> _fetchBalances() async { Future<ElectrumBalance> _fetchBalances() async {
@ -799,21 +1026,25 @@ abstract class ElectrumWalletBase
} }
var totalFrozen = 0; var totalFrozen = 0;
var totalConfirmed = 0;
var totalUnconfirmed = 0;
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
unspentCoinsInfo.values.forEach((info) { unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) { unspentCoins.forEach((element) {
if (element.hash == info.hash && if (element.hash == info.hash &&
element.vout == info.vout &&
info.isFrozen &&
element.bitcoinAddressRecord.address == info.address && element.bitcoinAddressRecord.address == info.address &&
element.value == info.value) { element.value == info.value) {
totalFrozen += element.value; if (info.isFrozen) totalFrozen += element.value;
if (element.bitcoinAddressRecord.silentPaymentTweak != null) {
totalConfirmed += element.value;
}
} }
}); });
}); });
final balances = await Future.wait(balanceFutures); final balances = await Future.wait(balanceFutures);
var totalConfirmed = 0;
var totalUnconfirmed = 0;
for (var i = 0; i < balances.length; i++) { for (var i = 0; i < balances.length; i++) {
final addressRecord = addresses[i]; final addressRecord = addresses[i];
@ -860,6 +1091,428 @@ abstract class ElectrumWalletBase
final HD = index == null ? hd : hd.derive(index); final HD = index == null ? hd : hd.derive(index);
return base64Encode(HD.signMessage(message)); return base64Encode(HD.signMessage(message));
} }
Future<void> _setInitialHeight() async {
if (walletInfo.isRecovery) {
return;
}
if (walletInfo.restoreHeight == 0) {
final currentHeight = await electrumClient.getCurrentBlockChainTip();
if (currentHeight != null) walletInfo.restoreHeight = currentHeight;
}
}
}
class ScanData {
final SendPort sendPort;
final SilentPaymentReceiver primarySilentAddress;
final int height;
final String node;
final bitcoin.NetworkType networkType;
final int chainTip;
final ElectrumClient electrumClient;
final List<String> transactionHistoryIds;
final Map<String, String> labels;
ScanData({
required this.sendPort,
required this.primarySilentAddress,
required this.height,
required this.node,
required this.networkType,
required this.chainTip,
required this.electrumClient,
required this.transactionHistoryIds,
required this.labels,
});
factory ScanData.fromHeight(ScanData scanData, int newHeight) {
return ScanData(
sendPort: scanData.sendPort,
primarySilentAddress: scanData.primarySilentAddress,
height: newHeight,
node: scanData.node,
networkType: scanData.networkType,
chainTip: scanData.chainTip,
transactionHistoryIds: scanData.transactionHistoryIds,
electrumClient: scanData.electrumClient,
labels: scanData.labels,
);
}
}
class SyncResponse {
final int height;
final SyncStatus syncStatus;
SyncResponse(this.height, this.syncStatus);
}
// Future<void> startRefresh(ScanData scanData) async {
// var cachedBlockchainHeight = scanData.chainTip;
// Future<int> getNodeHeightOrUpdate(int baseHeight) async {
// if (cachedBlockchainHeight < baseHeight || cachedBlockchainHeight == 0) {
// final electrumClient = scanData.electrumClient;
// if (!electrumClient.isConnected) {
// final node = scanData.node;
// await electrumClient.connectToUri(Uri.parse(node));
// }
// cachedBlockchainHeight =
// await electrumClient.getCurrentBlockChainTip() ?? cachedBlockchainHeight;
// }
// return cachedBlockchainHeight;
// }
// var lastKnownBlockHeight = 0;
// var initialSyncHeight = 0;
// var syncHeight = scanData.height;
// var currentChainTip = scanData.chainTip;
// if (syncHeight <= 0) {
// syncHeight = currentChainTip;
// }
// if (initialSyncHeight <= 0) {
// initialSyncHeight = syncHeight;
// }
// if (lastKnownBlockHeight == syncHeight) {
// scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus()));
// return;
// }
// // Run this until no more blocks left to scan txs. At first this was recursive
// // i.e. re-calling the startRefresh function but this was easier for the above values to retain
// // their initial values
// while (true) {
// lastKnownBlockHeight = syncHeight;
// final syncingStatus =
// SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight);
// scanData.sendPort.send(SyncResponse(syncHeight, syncingStatus));
// if (syncingStatus.blocksLeft <= 0) {
// scanData.sendPort.send(SyncResponse(currentChainTip, SyncedSyncStatus()));
// return;
// }
// // print(["Scanning from height:", syncHeight]);
// try {
// final networkPath =
// scanData.networkType.network == bitcoin.BtcNetwork.mainnet ? "" : "/testnet";
// // This endpoint gets up to 10 latest blocks from the given height
// final tenNewestBlocks =
// (await http.get(Uri.parse("https://blockstream.info$networkPath/api/blocks/$syncHeight")))
// .body;
// var decodedBlocks = json.decode(tenNewestBlocks) as List<dynamic>;
// decodedBlocks.sort((a, b) => (a["height"] as int).compareTo(b["height"] as int));
// decodedBlocks =
// decodedBlocks.where((element) => (element["height"] as int) >= syncHeight).toList();
// // for each block, get up to 25 txs
// for (var i = 0; i < decodedBlocks.length; i++) {
// final blockJson = decodedBlocks[i];
// final blockHash = blockJson["id"];
// final txCount = blockJson["tx_count"] as int;
// // print(["Scanning block index:", i, "with tx count:", txCount]);
// int startIndex = 0;
// // go through each tx in block until no more txs are left
// while (startIndex < txCount) {
// // This endpoint gets up to 25 txs from the given block hash and start index
// final twentyFiveTxs = json.decode((await http.get(Uri.parse(
// "https://blockstream.info$networkPath/api/block/$blockHash/txs/$startIndex")))
// .body) as List<dynamic>;
// // print(["Scanning txs index:", startIndex]);
// // For each tx, apply silent payment filtering and do shared secret calculation when applied
// for (var i = 0; i < twentyFiveTxs.length; i++) {
// try {
// final tx = twentyFiveTxs[i];
// final txid = tx["txid"] as String;
// // print(["Scanning tx:", txid]);
// // TODO: if tx already scanned & stored skip
// // if (scanData.transactionHistoryIds.contains(txid)) {
// // // already scanned tx, continue to next tx
// // pos++;
// // continue;
// // }
// List<String> pubkeys = [];
// List<bitcoin.Outpoint> outpoints = [];
// bool skip = false;
// for (var i = 0; i < (tx["vin"] as List<dynamic>).length; i++) {
// final input = tx["vin"][i];
// final prevout = input["prevout"];
// final scriptPubkeyType = prevout["scriptpubkey_type"];
// String? pubkey;
// if (scriptPubkeyType == "v0_p2wpkh" || scriptPubkeyType == "v1_p2tr") {
// final witness = input["witness"];
// if (witness == null) {
// skip = true;
// // print("Skipping, no witness");
// break;
// }
// if (witness.length == 2) {
// pubkey = witness[1] as String;
// } else if (witness.length == 1) {
// pubkey = "02" + (prevout["scriptpubkey"] as String).fromHex.sublist(2).hex;
// }
// }
// if (scriptPubkeyType == "p2pkh") {
// pubkey = bitcoin.P2pkhAddress(
// scriptSig: bitcoin.Script.fromRaw(hexData: input["scriptsig"] as String))
// .pubkey;
// }
// if (pubkey == null) {
// skip = true;
// // print("Skipping, invalid witness");
// break;
// }
// pubkeys.add(pubkey);
// outpoints.add(
// bitcoin.Outpoint(txid: input["txid"] as String, index: input["vout"] as int));
// }
// if (skip) {
// // skipped tx, continue to next tx
// continue;
// }
// Map<String, bitcoin.Outpoint> outpointsByP2TRpubkey = {};
// for (var i = 0; i < (tx["vout"] as List<dynamic>).length; i++) {
// final output = tx["vout"][i];
// if (output["scriptpubkey_type"] != "v1_p2tr") {
// // print("Skipping, not a v1_p2tr output");
// continue;
// }
// final script = (output["scriptpubkey"] as String).fromHex;
// // final alreadySpentOutput = (await electrumClient.getHistory(
// // scriptHashFromScript(script, networkType: scanData.networkType)))
// // .length >
// // 1;
// // if (alreadySpentOutput) {
// // print("Skipping, invalid witness");
// // break;
// // }
// final p2tr = bitcoin.P2trAddress(
// program: script.sublist(2).hex, networkType: scanData.networkType);
// final address = p2tr.address;
// print(["Verifying taproot address:", address]);
// outpointsByP2TRpubkey[script.sublist(2).hex] =
// bitcoin.Outpoint(txid: txid, index: i, value: output["value"] as int);
// }
// if (pubkeys.isEmpty || outpoints.isEmpty || outpointsByP2TRpubkey.isEmpty) {
// // skipped tx, continue to next tx
// continue;
// }
// final outpointHash = bitcoin.SilentPayment.hashOutpoints(outpoints);
// final result = bitcoin.scanOutputs(
// scanData.primarySilentAddress.scanPrivkey,
// scanData.primarySilentAddress.spendPubkey,
// bitcoin.getSumInputPubKeys(pubkeys),
// outpointHash,
// outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(),
// labels: scanData.labels,
// );
// if (result.isEmpty) {
// // no results tx, continue to next tx
// continue;
// }
// if (result.length > 1) {
// print("MULTIPLE UNSPENT COINS FOUND!");
// } else {
// print("UNSPENT COIN FOUND!");
// }
// result.forEach((key, value) async {
// final outpoint = outpointsByP2TRpubkey[key];
// if (outpoint == null) {
// return;
// }
// final tweak = value[0];
// String? label;
// if (value.length > 1) label = value[1];
// final txInfo = ElectrumTransactionInfo(
// WalletType.bitcoin,
// id: txid,
// height: syncHeight,
// amount: outpoint.value!,
// fee: 0,
// direction: TransactionDirection.incoming,
// isPending: false,
// date: DateTime.fromMillisecondsSinceEpoch((blockJson["timestamp"] as int) * 1000),
// confirmations: currentChainTip - syncHeight,
// to: bitcoin.SilentPaymentAddress.createLabeledSilentPaymentAddress(
// scanData.primarySilentAddress.scanPubkey,
// scanData.primarySilentAddress.spendPubkey,
// label != null ? label.fromHex : "0".fromHex,
// hrp: scanData.primarySilentAddress.hrp,
// version: scanData.primarySilentAddress.version)
// .toString(),
// unspent: null,
// );
// final status = json.decode((await http
// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$txid/outspends")))
// .body) as List<dynamic>;
// bool spent = false;
// for (final s in status) {
// if ((s["spent"] as bool) == true) {
// spent = true;
// scanData.sendPort.send({txid: txInfo});
// final sentTxId = s["txid"] as String;
// final sentTx = json.decode((await http
// .get(Uri.parse("https://blockstream.info/testnet/api/tx/$sentTxId")))
// .body);
// int amount = 0;
// for (final out in (sentTx["vout"] as List<dynamic>)) {
// amount += out["value"] as int;
// }
// final height = s["status"]["block_height"] as int;
// scanData.sendPort.send({
// sentTxId: ElectrumTransactionInfo(
// WalletType.bitcoin,
// id: sentTxId,
// height: height,
// amount: amount,
// fee: 0,
// direction: TransactionDirection.outgoing,
// isPending: false,
// date: DateTime.fromMillisecondsSinceEpoch(
// (s["status"]["block_time"] as int) * 1000),
// confirmations: currentChainTip - height,
// )
// });
// }
// }
// if (spent) {
// return;
// }
// final unspent = BitcoinUnspent(
// BitcoinAddressRecord(
// bitcoin.P2trAddress(program: key, networkType: scanData.networkType).address,
// index: 0,
// isHidden: true,
// isUsed: true,
// silentAddressLabel: null,
// silentPaymentTweak: tweak,
// type: bitcoin.AddressType.p2tr,
// ),
// txid,
// outpoint.value!,
// outpoint.index,
// silentPaymentTweak: tweak,
// type: bitcoin.AddressType.p2tr,
// );
// // found utxo for tx, send unspent coin to main isolate
// scanData.sendPort.send(unspent);
// // also send tx data for tx history
// txInfo.unspent = unspent;
// scanData.sendPort.send({txid: txInfo});
// });
// } catch (_) {}
// }
// // Finished scanning batch of txs in block, add 25 to start index and continue to next block in loop
// startIndex += 25;
// }
// // Finished scanning block, add 1 to height and continue to next block in loop
// syncHeight += 1;
// currentChainTip = await getNodeHeightOrUpdate(syncHeight);
// scanData.sendPort.send(SyncResponse(syncHeight,
// SyncingSyncStatus.fromHeightValues(currentChainTip, initialSyncHeight, syncHeight)));
// }
// } catch (e, stacktrace) {
// print(stacktrace);
// print(e.toString());
// scanData.sendPort.send(SyncResponse(syncHeight, NotConnectedSyncStatus()));
// break;
// }
// }
// }
class EstimatedTxResult {
EstimatedTxResult(
{required this.utxos, required this.privateKeys, required this.fee, required this.amount});
final List<UtxoWithAddress> utxos;
final List<ECPrivate> privateKeys;
final int fee;
final int amount;
}
BitcoinBaseAddress _addressTypeFromStr(String address, BasedUtxoNetwork network) {
if (P2pkhAddress.regex.hasMatch(address)) {
return P2pkhAddress.fromAddress(address: address, network: network);
} else if (P2shAddress.regex.hasMatch(address)) {
return P2shAddress.fromAddress(address: address, network: network);
} else if (P2wshAddress.regex.hasMatch(address)) {
return P2wshAddress.fromAddress(address: address, network: network);
} else if (P2trAddress.regex.hasMatch(address)) {
return P2trAddress.fromAddress(address: address, network: network);
} else {
return P2wpkhAddress.fromAddress(address: address, network: network);
}
}
BitcoinAddressType _getScriptType(BitcoinBaseAddress type) {
if (type is P2pkhAddress) {
return P2pkhAddressType.p2pkh;
} else if (type is P2shAddress) {
return P2shAddressType.p2wpkhInP2sh;
} else if (type is P2wshAddress) {
return SegwitAddresType.p2wsh;
} else if (type is P2trAddress) {
return SegwitAddresType.p2tr;
} else {
return SegwitAddresType.p2wpkh;
}
} }
class EstimateTxParams { class EstimateTxParams {

View file

@ -2,7 +2,6 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_addresses.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
@ -25,12 +24,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
WalletInfo walletInfo, { WalletInfo walletInfo, {
required this.mainHd, required this.mainHd,
required this.sideHd, required this.sideHd,
required this.electrumClient,
required this.network, required this.network,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
Map<String, int>? initialRegularAddressIndex, Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex, Map<String, int>? initialChangeAddressIndex,
List<BitcoinAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
SilentPaymentOwner? silentAddress,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()), }) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
primarySilentAddress = silentAddress,
addressesByReceiveType = addressesByReceiveType =
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()), ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []) receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
@ -44,6 +46,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
_addressPageType = walletInfo.addressPageType != null _addressPageType = walletInfo.addressPageType != null
? BitcoinAddressType.fromValue(walletInfo.addressPageType!) ? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
: SegwitAddresType.p2wpkh, : SegwitAddresType.p2wpkh,
silentAddresses = ObservableList<BitcoinAddressRecord>.of((initialSilentAddresses ?? [])
.where((addressRecord) => addressRecord.silentPaymentTweak != null)
.toSet()),
currentSilentAddressIndex = initialSilentAddressIndex,
super(walletInfo) { super(walletInfo) {
updateAddressesByMatch(); updateAddressesByMatch();
} }
@ -61,27 +67,57 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
late ObservableList<BitcoinAddressRecord> addressesByReceiveType; late ObservableList<BitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses; final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses; final ObservableList<BitcoinAddressRecord> changeAddresses;
final ElectrumClient electrumClient; final ObservableList<BitcoinAddressRecord> silentAddresses;
final BasedUtxoNetwork network; final BasedUtxoNetwork network;
final bitcoin.HDWallet mainHd; final bitcoin.HDWallet mainHd;
final bitcoin.HDWallet sideHd; final bitcoin.HDWallet sideHd;
final SilentPaymentOwner? primarySilentAddress;
@observable @observable
BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh; BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh;
@computed @computed
BitcoinAddressType get addressPageType => _addressPageType; BitcoinAddressType get addressPageType => _addressPageType;
@observable
String? activeSilentAddress;
@computed @computed
List<BitcoinAddressRecord> get allAddresses => _addresses; List<BitcoinAddressRecord> get allAddresses => _addresses;
@override @override
@computed @computed
String get address { String get address {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
if (activeSilentAddress != null) {
return activeSilentAddress!;
}
return primarySilentAddress!.toString();
}
String receiveAddress; String receiveAddress;
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch); final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) {
receiveAddress = generateNewAddress().address;
} else {
final previousAddressMatchesType =
previousAddressRecord != null && previousAddressRecord!.type == addressPageType;
if (previousAddressMatchesType &&
typeMatchingReceiveAddresses.first.address != addressesByReceiveType.first.address) {
receiveAddress = previousAddressRecord!.address;
} else {
receiveAddress = typeMatchingReceiveAddresses.first.address;
}
final receiveAddress = receiveAddresses.first.address;
final typeMatchingReceiveAddresses = receiveAddresses.where(_isAddressPageTypeMatch);
if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) || if ((isEnabledAutoGenerateSubaddress && receiveAddresses.isEmpty) ||
typeMatchingReceiveAddresses.isEmpty) { typeMatchingReceiveAddresses.isEmpty) {
receiveAddress = generateNewAddress().address; receiveAddress = generateNewAddress().address;
@ -105,6 +141,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override @override
set address(String addr) { set address(String addr) {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
activeSilentAddress = addr;
return;
}
if (addr.startsWith('bitcoincash:')) { if (addr.startsWith('bitcoincash:')) {
addr = toLegacy(addr); addr = toLegacy(addr);
} }
@ -134,6 +175,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
void set currentChangeAddressIndex(int index) => void set currentChangeAddressIndex(int index) =>
currentChangeAddressIndexByType[_addressPageType.toString()] = index; currentChangeAddressIndexByType[_addressPageType.toString()] = index;
int currentSilentAddressIndex;
@observable @observable
BitcoinAddressRecord? previousAddressRecord; BitcoinAddressRecord? previousAddressRecord;
@ -195,7 +238,43 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address; return address;
} }
Map<String, String> get labels {
final labels = <String, String>{};
for (int i = 0; i < silentAddresses.length; i++) {
final silentAddressRecord = silentAddresses[i];
final silentAddress =
SilentPaymentDestination.fromAddress(silentAddressRecord.address, 0).spendPubkey.toHex();
if (silentAddressRecord.silentPaymentTweak != null)
labels[silentAddress] = silentAddressRecord.silentPaymentTweak!;
}
return labels;
}
BitcoinAddressRecord generateNewAddress({String label = ''}) { BitcoinAddressRecord generateNewAddress({String label = ''}) {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
currentSilentAddressIndex += 1;
final tweak = BigInt.from(currentSilentAddressIndex);
final address = BitcoinAddressRecord(
SilentPaymentAddress.createLabeledSilentPaymentAddress(
primarySilentAddress!.scanPubkey, primarySilentAddress!.spendPubkey, tweak,
hrp: primarySilentAddress!.hrp, version: primarySilentAddress!.version)
.toString(),
index: currentSilentAddressIndex,
isHidden: false,
name: label,
silentPaymentTweak: tweak.toString(),
network: network,
type: SilentPaymentsAddresType.p2sp,
);
silentAddresses.add(address);
return address;
}
final newAddressIndex = addressesByReceiveType.fold( final newAddressIndex = addressesByReceiveType.fold(
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);

View file

@ -13,11 +13,13 @@ class ElectrumWalletSnapshot {
required this.password, required this.password,
required this.mnemonic, required this.mnemonic,
required this.addresses, required this.addresses,
required this.silentAddresses,
required this.balance, required this.balance,
required this.regularAddressIndex, required this.regularAddressIndex,
required this.changeAddressIndex, required this.changeAddressIndex,
required this.addressPageType, required this.addressPageType,
required this.network, required this.network,
required this.silentAddressIndex,
}); });
final String name; final String name;
@ -28,24 +30,36 @@ class ElectrumWalletSnapshot {
String mnemonic; String mnemonic;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
List<BitcoinAddressRecord> silentAddresses;
ElectrumBalance balance; ElectrumBalance balance;
Map<String, int> regularAddressIndex; Map<String, int> regularAddressIndex;
Map<String, int> changeAddressIndex; Map<String, int> changeAddressIndex;
int silentAddressIndex;
static Future<ElectrumWalletSnapshot> load(String name, WalletType type, String password, BasedUtxoNetwork? network) async { static Future<ElectrumWalletSnapshot> load(
String name, WalletType type, String password, BasedUtxoNetwork? network) async {
final path = await pathForWallet(name: name, type: type); final path = await pathForWallet(name: name, type: type);
final jsonSource = await read(path: path, password: password); final jsonSource = await read(path: path, password: password);
final data = json.decode(jsonSource) as Map; final data = json.decode(jsonSource) as Map;
final addressesTmp = data['addresses'] as List? ?? <Object>[];
final mnemonic = data['mnemonic'] as String; final mnemonic = data['mnemonic'] as String;
final addressesTmp = data['addresses'] as List? ?? <Object>[];
final addresses = addressesTmp final addresses = addressesTmp
.whereType<String>() .whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network)) .map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
.toList(); .toList();
final silentAddressesTmp = data['silent_addresses'] as List? ?? <Object>[];
final silentAddresses = silentAddressesTmp
.whereType<String>()
.map((addr) => BitcoinAddressRecord.fromJSON(addr, network: network))
.toList();
final balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? final balance = ElectrumBalance.fromJSON(data['balance'] as String) ??
ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0); ElectrumBalance(confirmed: 0, unconfirmed: 0, frozen: 0);
var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0}; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
var silentAddressIndex = 0;
try { try {
regularAddressIndexByType = { regularAddressIndexByType = {
@ -55,6 +69,7 @@ class ElectrumWalletSnapshot {
SegwitAddresType.p2wpkh.toString(): SegwitAddresType.p2wpkh.toString():
int.parse(data['change_address_index'] as String? ?? '0') int.parse(data['change_address_index'] as String? ?? '0')
}; };
silentAddressIndex = int.parse(data['silent_address_index'] as String? ?? '0');
} catch (_) { } catch (_) {
try { try {
regularAddressIndexByType = data["account_index"] as Map<String, int>? ?? {}; regularAddressIndexByType = data["account_index"] as Map<String, int>? ?? {};
@ -68,11 +83,13 @@ class ElectrumWalletSnapshot {
password: password, password: password,
mnemonic: mnemonic, mnemonic: mnemonic,
addresses: addresses, addresses: addresses,
silentAddresses: silentAddresses,
balance: balance, balance: balance,
regularAddressIndex: regularAddressIndexByType, regularAddressIndex: regularAddressIndexByType,
changeAddressIndex: changeAddressIndexByType, changeAddressIndex: changeAddressIndexByType,
addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(), addressPageType: data['address_page_type'] as String? ?? SegwitAddresType.p2wpkh.toString(),
network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet, network: data['network_type'] == 'testnet' ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet,
silentAddressIndex: silentAddressIndex,
); );
} }
} }

View file

@ -79,8 +79,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: cake-update-v1 ref: cake-update-v2
resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa" resolved-ref: e4686da77cace5400697de69f7885020297cb900
url: "https://github.com/cake-tech/bitcoin_base.git" url: "https://github.com/cake-tech/bitcoin_base.git"
source: git source: git
version: "4.0.0" version: "4.0.0"
@ -93,14 +93,6 @@ packages:
url: "https://github.com/cake-tech/bitcoin_flutter.git" url: "https://github.com/cake-tech/bitcoin_flutter.git"
source: git source: git
version: "2.1.0" version: "2.1.0"
blockchain_utils:
dependency: "direct main"
description:
name: blockchain_utils
sha256: "9701dfaa74caad4daae1785f1ec4445cf7fb94e45620bc3a4aca1b9b281dc6c9"
url: "https://pub.dev"
source: hosted
version: "1.6.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -33,8 +33,7 @@ dependencies:
bitcoin_base: bitcoin_base:
git: git:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v1 ref: cake-update-v2
blockchain_utils: ^1.6.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -32,7 +32,7 @@ dependencies:
bitcoin_base: bitcoin_base:
git: git:
url: https://github.com/cake-tech/bitcoin_base.git url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v1 ref: cake-update-v2

View file

@ -14,6 +14,16 @@ class SyncingSyncStatus extends SyncStatus {
@override @override
String toString() => '$blocksLeft'; String toString() => '$blocksLeft';
factory SyncingSyncStatus.fromHeightValues(int chainTip, int initialSyncHeight, int syncHeight) {
final track = chainTip - initialSyncHeight;
final diff = track - (chainTip - syncHeight);
final ptc = diff <= 0 ? 0.0 : diff / track;
final left = chainTip - syncHeight;
// sum 1 because if at the chain tip, will say "0 blocks left"
return SyncingSyncStatus(left + 1, ptc);
}
} }
class SyncedSyncStatus extends SyncStatus { class SyncedSyncStatus extends SyncStatus {
@ -51,4 +61,6 @@ class ConnectedSyncStatus extends SyncStatus {
class LostConnectionSyncStatus extends SyncStatus { class LostConnectionSyncStatus extends SyncStatus {
@override @override
double progress() => 1.0; double progress() => 1.0;
} @override
String toString() => 'Reconnecting';
}

View file

@ -16,5 +16,6 @@ class Unspent {
bool isFrozen; bool isFrozen;
String note; String note;
bool get isP2wpkh => address.startsWith('bc') || address.startsWith('ltc'); bool get isP2wpkh =>
address.startsWith('bc') || address.startsWith('tb') || address.startsWith('ltc');
} }

View file

@ -142,27 +142,9 @@ Then we need to generate localization files.
`$ flutter packages pub run tool/generate_localization.dart` `$ flutter packages pub run tool/generate_localization.dart`
Lastly, we will generate mobx models for the project.
Generate mobx models for `cw_core`:
`cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Generate mobx models for `cw_monero`:
`cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Generate mobx models for `cw_bitcoin`:
`cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Generate mobx models for `cw_haven`:
`cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..`
Finally build mobx models for the app: Finally build mobx models for the app:
`$ flutter packages pub run build_runner build --delete-conflicting-outputs` `$ ./model_generator.sh`
### 9. Build! ### 9. Build!

View file

@ -188,4 +188,9 @@ class CWBitcoin extends Bitcoin {
@override @override
List<BitcoinReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all; List<BitcoinReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
List<BitcoinAddressRecord> getSilentAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.silentAddresses;
}
} }

View file

@ -26,7 +26,7 @@ class AddressValidator extends TextValidator {
return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$' return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$'
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$'; '|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
case CryptoCurrency.btc: case CryptoCurrency.btc:
return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$'; return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${bitcoin.SilentPaymentAddress.REGEX.pattern}\$';
case CryptoCurrency.nano: case CryptoCurrency.nano:
return '[0-9a-zA-Z_]'; return '[0-9a-zA-Z_]';
case CryptoCurrency.banano: case CryptoCurrency.banano:
@ -274,7 +274,8 @@ class AddressValidator extends TextValidator {
'([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)' '([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)' '([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)' '([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)'; '([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)'
'|${bitcoin.SilentPaymentAddress.REGEX.pattern}\$';
case CryptoCurrency.ltc: case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'

View file

@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart';
String syncStatusTitle(SyncStatus syncStatus) { String syncStatusTitle(SyncStatus syncStatus) {
if (syncStatus is SyncingSyncStatus) { if (syncStatus is SyncingSyncStatus) {
return S.current.Blocks_remaining('${syncStatus.blocksLeft}'); return syncStatus.blocksLeft == 1
? S.current.Block_remaining('${syncStatus.blocksLeft}')
: S.current.Blocks_remaining('${syncStatus.blocksLeft}');
} }
if (syncStatus is SyncedSyncStatus) { if (syncStatus is SyncedSyncStatus) {
@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) {
} }
return ''; return '';
} }

View file

@ -229,6 +229,7 @@ import 'package:cake_wallet/src/screens/receive/fullscreen_qr_page.dart';
import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as btc;
import 'buy/dfx/dfx_buy_provider.dart'; import 'buy/dfx/dfx_buy_provider.dart';
import 'core/totp_request_details.dart'; import 'core/totp_request_details.dart';
@ -657,6 +658,10 @@ Future<void> setup({
getIt.registerFactory<MoneroAccountListViewModel>(() { getIt.registerFactory<MoneroAccountListViewModel>(() {
final wallet = getIt.get<AppStore>().wallet!; final wallet = getIt.get<AppStore>().wallet!;
// if ((wallet.type == WalletType.bitcoin &&
// wallet.walletAddresses.addressPageType == btc.AddressType.p2sp) ||
// wallet.type == WalletType.monero ||
// wallet.type == WalletType.haven) {
if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) { if (wallet.type == WalletType.monero || wallet.type == WalletType.haven) {
return MoneroAccountListViewModel(wallet); return MoneroAccountListViewModel(wallet);
} }

View file

@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
class PresentReceiveOptionPicker extends StatelessWidget { class PresentReceiveOptionPicker extends StatelessWidget {
PresentReceiveOptionPicker( PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color});
{required this.receiveOptionViewModel, required this.color});
final ReceiveOptionViewModel receiveOptionViewModel; final ReceiveOptionViewModel receiveOptionViewModel;
final Color color; final Color color;
@ -43,17 +42,11 @@ class PresentReceiveOptionPicker extends StatelessWidget {
Text( Text(
S.current.receive, S.current.receive,
style: TextStyle( style: TextStyle(
fontSize: 18.0, fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color),
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: color),
), ),
Observer( Observer(
builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(), builder: (_) => Text(describeOption(receiveOptionViewModel.selectedReceiveOption),
style: TextStyle( style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color)))
fontSize: 10.0,
fontWeight: FontWeight.w500,
color: color)))
], ],
), ),
SizedBox(width: 5), SizedBox(width: 5),
@ -73,65 +66,68 @@ class PresentReceiveOptionPicker extends StatelessWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
body: Stack( body: Stack(
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
children:[ AlertBackground( children: [
child: Column( AlertBackground(
mainAxisSize: MainAxisSize.min, child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min,
children: [ mainAxisAlignment: MainAxisAlignment.center,
Spacer(), children: [
Container( Spacer(),
margin: EdgeInsets.symmetric(horizontal: 24), Container(
decoration: BoxDecoration( margin: EdgeInsets.symmetric(horizontal: 24),
borderRadius: BorderRadius.circular(30), decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(30),
), color: Theme.of(context).colorScheme.background,
child: Padding( ),
padding: const EdgeInsets.only(top: 24, bottom: 24), child: Padding(
child: (ListView.separated( padding: const EdgeInsets.only(top: 24, bottom: 24),
padding: EdgeInsets.zero, child: (ListView.separated(
shrinkWrap: true, padding: EdgeInsets.zero,
itemCount: receiveOptionViewModel.options.length, shrinkWrap: true,
itemBuilder: (_, index) { itemCount: receiveOptionViewModel.options.length,
final option = receiveOptionViewModel.options[index]; itemBuilder: (_, index) {
return InkWell( final option = receiveOptionViewModel.options[index];
onTap: () { return InkWell(
Navigator.pop(popUpContext); onTap: () {
Navigator.pop(popUpContext);
receiveOptionViewModel.selectReceiveOption(option); receiveOptionViewModel.selectReceiveOption(option);
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24), padding: const EdgeInsets.only(left: 24, right: 24),
child: Observer(builder: (_) { child: Observer(builder: (_) {
final value = receiveOptionViewModel.selectedReceiveOption; final value = receiveOptionViewModel.selectedReceiveOption;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(option.toString(), Text(describeOption(option),
textAlign: TextAlign.left, textAlign: TextAlign.left,
style: textSmall( style: textSmall(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context)
).copyWith( .extension<CakeTextTheme>()!
fontWeight: .titleColor,
value == option ? FontWeight.w800 : FontWeight.w500, ).copyWith(
)), fontWeight:
RoundedCheckbox( value == option ? FontWeight.w800 : FontWeight.w500,
value: value == option, )),
) RoundedCheckbox(
], value: value == option,
); )
}), ],
), );
); }),
}, ),
separatorBuilder: (_, index) => SizedBox(height: 30), );
)), },
separatorBuilder: (_, index) => SizedBox(height: 30),
)),
),
), ),
), Spacer()
Spacer() ],
], ),
), ),
),
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40) AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
], ],
), ),

View file

@ -67,8 +67,7 @@ class ReceivePage extends BasePage {
@override @override
Widget Function(BuildContext, Widget) get rootWrapper => Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) => (BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
GradientBackground(scaffold: scaffold);
@override @override
Widget trailing(BuildContext context) { Widget trailing(BuildContext context) {
@ -159,7 +158,8 @@ class ReceivePage extends BasePage {
trailingIcon: Icon( trailingIcon: Icon(
Icons.arrow_forward_ios, Icons.arrow_forward_ios,
size: 14, size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor, color:
Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
)); ));
} }
@ -185,11 +185,19 @@ class ReceivePage extends BasePage {
final isCurrent = final isCurrent =
item.address == addressListViewModel.address.address; item.address == addressListViewModel.address.address;
final backgroundColor = isCurrent final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor ? Theme.of(context)
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor; .extension<ReceivePageTheme>()!
.currentTileBackgroundColor
: Theme.of(context)
.extension<ReceivePageTheme>()!
.tilesBackgroundColor;
final textColor = isCurrent final textColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor ? Theme.of(context)
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor; .extension<ReceivePageTheme>()!
.currentTileTextColor
: Theme.of(context)
.extension<ReceivePageTheme>()!
.tilesTextColor;
return AddressCell.fromItem(item, return AddressCell.fromItem(item,
isCurrent: isCurrent, isCurrent: isCurrent,
@ -211,6 +219,16 @@ class ReceivePage extends BasePage {
child: cell, child: cell,
); );
})), })),
if (!addressListViewModel.hasSilentAddresses)
Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(S.of(context).electrum_address_disclaimer,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color:
Theme.of(context).extension<BalancePageTheme>()!.labelTextColor)),
),
], ],
), ),
)) ))

View file

@ -2,6 +2,7 @@ import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/crypto_currency.dart';
@ -164,7 +165,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
}, },
options: [ options: [
AddressTextFieldOption.paste, AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode, if (DeviceInfo.instance.isMobile) AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook AddressTextFieldOption.addressBook
], ],
buttonColor: buttonColor:

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -133,8 +134,7 @@ class AddressTextField extends StatelessWidget {
), ),
)), )),
], ],
if (this.options.contains(AddressTextFieldOption.qrCode) && if (this.options.contains(AddressTextFieldOption.qrCode)) ...[
DeviceInfo.instance.isMobile) ...[
Container( Container(
width: prefixIconWidth, width: prefixIconWidth,
height: prefixIconHeight, height: prefixIconHeight,
@ -194,7 +194,7 @@ class AddressTextField extends StatelessWidget {
Future<void> _presentQRScanner(BuildContext context) async { Future<void> _presentQRScanner(BuildContext context) async {
bool isCameraPermissionGranted = bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context); await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return; if (!isCameraPermissionGranted) return;
final code = await presentQRScanner(); final code = await presentQRScanner();
if (code.isEmpty) { if (code.isEmpty) {

View file

@ -278,6 +278,11 @@ abstract class DashboardViewModelBase with Store {
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet; WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven; bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven;
// bool get hasRescan =>
// (wallet.type == WalletType.bitcoin &&
// wallet.walletAddresses.addressPageType == bitcoin.AddressType.p2sp) ||
// wallet.type == WalletType.monero ||
// wallet.type == WalletType.haven;
final KeyService keyService; final KeyService keyService;

View file

@ -1,4 +1,5 @@
import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
part 'rescan_view_model.g.dart'; part 'rescan_view_model.g.dart';
@ -9,8 +10,8 @@ enum RescanWalletState { rescaning, none }
abstract class RescanViewModelBase with Store { abstract class RescanViewModelBase with Store {
RescanViewModelBase(this._wallet) RescanViewModelBase(this._wallet)
: state = RescanWalletState.none, : state = RescanWalletState.none,
isButtonEnabled = false; isButtonEnabled = false;
final WalletBase _wallet; final WalletBase _wallet;
@ -23,8 +24,8 @@ abstract class RescanViewModelBase with Store {
@action @action
Future<void> rescanCurrentWallet({required int restoreHeight}) async { Future<void> rescanCurrentWallet({required int restoreHeight}) async {
state = RescanWalletState.rescaning; state = RescanWalletState.rescaning;
await _wallet.rescan(height: restoreHeight); _wallet.rescan(height: restoreHeight);
_wallet.transactionHistory.clear(); if (_wallet.type != WalletType.bitcoin) _wallet.transactionHistory.clear();
state = RescanWalletState.none; state = RescanWalletState.none;
} }
} }

View file

@ -183,8 +183,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}) : _baseItems = <ListItem>[], }) : _baseItems = <ListItem>[],
selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type), selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
_cryptoNumberFormat = NumberFormat(_cryptoNumberPattern), _cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
hasAccounts =
appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
amount = '', amount = '',
_settingsStore = appStore.settingsStore, _settingsStore = appStore.settingsStore,
super(appStore: appStore) { super(appStore: appStore) {
@ -196,7 +194,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
_init(); _init();
selectedCurrency = walletTypeToCryptoCurrency(wallet.type); selectedCurrency = walletTypeToCryptoCurrency(wallet.type);
hasAccounts = wallet.type == WalletType.monero || wallet.type == WalletType.haven; _hasAccounts =
hasSilentAddresses || wallet.type == WalletType.monero || wallet.type == WalletType.haven;
} }
static const String _cryptoNumberPattern = '0.00000000'; static const String _cryptoNumberPattern = '0.00000000';
@ -365,7 +364,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
} }
@observable @observable
bool hasAccounts; bool _hasAccounts = false;
@computed
bool get hasAccounts => _hasAccounts;
@computed @computed
String get accountLabel { String get accountLabel {
@ -380,8 +382,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
return ''; return '';
} }
@observable
// ignore: prefer_final_fields
bool? _hasSilentAddresses = null;
@computed
bool get hasSilentAddresses => _hasSilentAddresses ?? wallet.type == WalletType.bitcoin;
// @computed
// bool get hasSilentAddresses =>
// _hasSilentAddresses ??
// wallet.type == WalletType.bitcoin &&
// wallet.walletAddresses.addressPageType == btc.AddressType.p2sp;
@computed @computed
bool get hasAddressList => bool get hasAddressList =>
hasSilentAddresses ||
wallet.type == WalletType.monero || wallet.type == WalletType.monero ||
wallet.type == WalletType.haven || wallet.type == WalletType.haven ||
wallet.type == WalletType.bitcoinCash || wallet.type == WalletType.bitcoinCash ||

View file

@ -1,11 +1,10 @@
cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_core; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_evm; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_monero; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash; flutter pub get; flutter packages pub run build_runner build --delete-conflicting-outputs; cd ..
cd cw_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_polygon; flutter pub get; cd ..
cd cw_ethereum && flutter pub get && cd .. cd cw_ethereum; flutter pub get; cd ..
cd cw_polygon && flutter pub get && cd ..
flutter packages pub run build_runner build --delete-conflicting-outputs flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -125,6 +125,7 @@ abstract class Bitcoin {
List<String> getAddresses(Object wallet); List<String> getAddresses(Object wallet);
String getAddress(Object wallet); String getAddress(Object wallet);
List<BitcoinAddressRecord> getSilentAddresses(Object wallet);
List<ElectrumSubAddress> getSubAddresses(Object wallet); List<ElectrumSubAddress> getSubAddresses(Object wallet);