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,
String? scriptHash,
required this.network,
this.silentPaymentTweak,
}) : _txCount = txCount,
_balance = balance,
_name = name,
@ -23,7 +24,7 @@ class BitcoinAddressRecord {
scriptHash =
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;
return BitcoinAddressRecord(
@ -42,6 +43,7 @@ class BitcoinAddressRecord {
network: (decoded['network'] as String?) == null
? network
: BasedUtxoNetwork.fromName(decoded['network'] as String),
silentPaymentTweak: decoded['silentPaymentTweak'] as String?,
);
}
@ -57,6 +59,7 @@ class BitcoinAddressRecord {
bool _isUsed;
String? scriptHash;
BasedUtxoNetwork? network;
final String? silentPaymentTweak;
int get txCount => _txCount;
@ -96,5 +99,6 @@ class BitcoinAddressRecord {
'type': type.toString(),
'scriptHash': scriptHash,
'network': network?.value,
'silentPaymentTweak': silentPaymentTweak,
});
}

View file

@ -8,6 +8,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
static const p2wsh = BitcoinReceivePageOption._('Segwit (P2WSH)');
static const p2pkh = BitcoinReceivePageOption._('Legacy (P2PKH)');
static const silent_payments = BitcoinReceivePageOption._('Silent Payments');
const BitcoinReceivePageOption._(this.value);
final String value;
@ -34,6 +36,8 @@ class BitcoinReceivePageOption implements ReceivePageOption {
return BitcoinReceivePageOption.p2pkh;
case P2shAddressType.p2wpkhInP2sh:
return BitcoinReceivePageOption.p2sh;
case SilentPaymentsAddresType.p2sp:
return BitcoinReceivePageOption.silent_payments;
case SegwitAddresType.p2wpkh:
default:
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_core/unspent_transaction_output.dart';
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,
super(addressRecord.address, hash, value, vout, null);
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) =>
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;
String? silentPaymentTweak;
BitcoinAddressType? type;
}

View file

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

View file

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

View file

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

View file

@ -1,9 +1,8 @@
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/bitcoin_address_record.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_info.dart';
import 'package:cw_core/format_amount.dart';
@ -20,6 +19,8 @@ class ElectrumTransactionBundle {
}
class ElectrumTransactionInfo extends TransactionInfo {
BitcoinUnspent? unspent;
ElectrumTransactionInfo(this.type,
{required String id,
required int height,
@ -28,7 +29,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
required TransactionDirection direction,
required bool isPending,
required DateTime date,
required int confirmations}) {
required int confirmations,
String? to,
this.unspent}) {
this.id = id;
this.height = height;
this.amount = amount;
@ -37,6 +40,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.date = date;
this.isPending = isPending;
this.confirmations = confirmations;
this.to = to;
}
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
@ -144,42 +148,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
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) {
return ElectrumTransactionInfo(type,
return ElectrumTransactionInfo(
type,
id: data['id'] as String,
height: data['height'] as int,
amount: data['amount'] as int,
@ -187,7 +158,14 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int);
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;
@ -231,6 +209,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending;
m['confirmations'] = confirmations;
m['fee'] = fee;
m['to'] = to;
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
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:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:bitcoin_base/bitcoin_base.dart';
@ -143,21 +144,93 @@ abstract class ElectrumWalletBase
List<BitcoinUnspent> unspentCoins;
List<int> _feeRates;
Map<String, BehaviorSubject<Object>?> _scripthashesUpdateSubject;
BehaviorSubject<Object>? _chainTipUpdateSubject;
bool _isTransactionUpdating;
// Future<Isolate>? _isolate;
void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer;
static const int _autoSaveInterval = 30;
Future<void> init() async {
await walletAddresses.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
@override
Future<void> startSync() async {
try {
syncStatus = AttemptingSyncStatus();
await _setInitialHeight();
} catch (_) {}
try {
rescan(height: walletInfo.restoreHeight);
await updateTransactions();
_subscribeForUpdates();
await updateUnspent();
@ -187,6 +260,12 @@ abstract class ElectrumWalletBase
}
};
syncStatus = ConnectedSyncStatus();
// final currentChainTip = await electrumClient.getCurrentBlockChainTip();
// if ((currentChainTip ?? 0) > walletInfo.restoreHeight) {
// _setListeners(walletInfo.restoreHeight, chainTip: currentChainTip);
// }
} catch (e) {
print(e.toString());
syncStatus = FailedSyncStatus();
@ -213,6 +292,124 @@ abstract class ElectrumWalletBase
allInputsAmount += 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 privkey = generateECPrivate(
hd: utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
@ -304,24 +501,23 @@ 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!));
// generatedOutputs.forEach((recipientSilentAddress, generatedOutput) {
// generatedOutput.forEach((output) {
// outputs.add(BitcoinOutputDetails(
// address: P2trAddress(
// program: ECPublic.fromHex(output.$1.toHex()).toTapPoint(),
// networkType: networkType),
// value: BigInt.from(output.$2),
// ));
// });
// });
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
final outputs = <BitcoinOutput>[];
final outputAddresses = <BitcoinBaseAddress>[];
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final hasMultiDestination = transactionCredentials.outputs.length > 1;
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);
if (hasMultiDestination) {
@ -392,6 +588,8 @@ abstract class ElectrumWalletBase
? SegwitAddresType.p2wpkh.toString()
: walletInfo.addressPageType.toString(),
'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',
});
@ -498,18 +696,31 @@ abstract class ElectrumWalletBase
}
@override
Future<void> rescan({required int height}) async => throw UnimplementedError();
Future<void> rescan({required int height, int? chainTip, ScanData? scanData}) async {
// _setListeners(height);
}
@override
Future<void> close() async {
try {
await electrumClient.close();
} catch (_) {}
_autoSaveTimer?.cancel();
}
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
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 = [];
final addressesSet = walletAddresses.allAddresses.map((addr) => addr.address).toSet();
@ -538,7 +749,7 @@ abstract class ElectrumWalletBase
final coinInfoList = unspentCoinsInfo.values.where((element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout);
element.address.contains(coin.address));
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
@ -555,6 +766,7 @@ abstract class ElectrumWalletBase
await _refreshUnspentCoinsInfo();
}
@action
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo(
walletId: id,
@ -619,19 +831,17 @@ abstract class ElectrumWalletBase
confirmations = verboseTransaction['confirmations'] as int? ?? 0;
}
final original = bitcoin_base.BtcTransaction.fromRaw(transactionHex);
final ins = <bitcoin_base.BtcTransaction>[];
final original = BtcTransaction.fromRaw(transactionHex);
final ins = <BtcTransaction>[];
for (final vin in original.inputs) {
try {
final id = HEX.encode(HEX.decode(vin.txId).reversed.toList());
final txHex = await electrumClient.getTransactionHex(hash: id);
final tx = bitcoin_base.BtcTransaction.fromRaw(txHex);
final tx = BtcTransaction.fromRaw(txHex);
ins.add(tx);
} catch (_) {
ins.add(bitcoin_base.BtcTransaction.fromRaw(
await electrumClient.getTransactionHex(hash: vin.txId),
));
ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId)));
}
}
@ -767,7 +977,7 @@ abstract class ElectrumWalletBase
}
}
void _subscribeForUpdates() {
void _subscribeForUpdates() async {
scriptHashes.forEach((sh) async {
await _scripthashesUpdateSubject[sh]?.close();
_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 {
@ -799,21 +1026,25 @@ abstract class ElectrumWalletBase
}
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) {
unspentCoins.forEach((element) {
if (element.hash == info.hash &&
element.vout == info.vout &&
info.isFrozen &&
element.bitcoinAddressRecord.address == info.address &&
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);
var totalConfirmed = 0;
var totalUnconfirmed = 0;
for (var i = 0; i < balances.length; i++) {
final addressRecord = addresses[i];
@ -860,6 +1091,428 @@ abstract class ElectrumWalletBase
final HD = index == null ? hd : hd.derive(index);
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 {

View file

@ -2,7 +2,6 @@ import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitbox/bitbox.dart' as bitbox;
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_info.dart';
import 'package:cw_core/wallet_type.dart';
@ -25,12 +24,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
WalletInfo walletInfo, {
required this.mainHd,
required this.sideHd,
required this.electrumClient,
required this.network,
List<BitcoinAddressRecord>? initialAddresses,
Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
List<BitcoinAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
SilentPaymentOwner? silentAddress,
}) : _addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()),
primarySilentAddress = silentAddress,
addressesByReceiveType =
ObservableList<BitcoinAddressRecord>.of((<BitcoinAddressRecord>[]).toSet()),
receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
@ -44,6 +46,10 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
_addressPageType = walletInfo.addressPageType != null
? BitcoinAddressType.fromValue(walletInfo.addressPageType!)
: SegwitAddresType.p2wpkh,
silentAddresses = ObservableList<BitcoinAddressRecord>.of((initialSilentAddresses ?? [])
.where((addressRecord) => addressRecord.silentPaymentTweak != null)
.toSet()),
currentSilentAddressIndex = initialSilentAddressIndex,
super(walletInfo) {
updateAddressesByMatch();
}
@ -61,27 +67,57 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
late ObservableList<BitcoinAddressRecord> addressesByReceiveType;
final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses;
final ElectrumClient electrumClient;
final ObservableList<BitcoinAddressRecord> silentAddresses;
final BasedUtxoNetwork network;
final bitcoin.HDWallet mainHd;
final bitcoin.HDWallet sideHd;
final SilentPaymentOwner? primarySilentAddress;
@observable
BitcoinAddressType _addressPageType = SegwitAddresType.p2wpkh;
@computed
BitcoinAddressType get addressPageType => _addressPageType;
@observable
String? activeSilentAddress;
@computed
List<BitcoinAddressRecord> get allAddresses => _addresses;
@override
@computed
String get address {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
if (activeSilentAddress != null) {
return activeSilentAddress!;
}
return primarySilentAddress!.toString();
}
String receiveAddress;
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) ||
typeMatchingReceiveAddresses.isEmpty) {
receiveAddress = generateNewAddress().address;
@ -105,6 +141,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
set address(String addr) {
if (addressPageType == SilentPaymentsAddresType.p2sp) {
activeSilentAddress = addr;
return;
}
if (addr.startsWith('bitcoincash:')) {
addr = toLegacy(addr);
}
@ -134,6 +175,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
void set currentChangeAddressIndex(int index) =>
currentChangeAddressIndexByType[_addressPageType.toString()] = index;
int currentSilentAddressIndex;
@observable
BitcoinAddressRecord? previousAddressRecord;
@ -195,7 +238,43 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
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 = ''}) {
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(
0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc);

View file

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

View file

@ -79,8 +79,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: cake-update-v1
resolved-ref: "9611e9db77e92a8434e918cdfb620068f6fcb1aa"
ref: cake-update-v2
resolved-ref: e4686da77cace5400697de69f7885020297cb900
url: "https://github.com/cake-tech/bitcoin_base.git"
source: git
version: "4.0.0"
@ -93,14 +93,6 @@ packages:
url: "https://github.com/cake-tech/bitcoin_flutter.git"
source: git
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:
dependency: transitive
description:

View file

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

View file

@ -32,7 +32,7 @@ dependencies:
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
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 {
@ -51,4 +61,6 @@ class ConnectedSyncStatus extends SyncStatus {
class LostConnectionSyncStatus extends SyncStatus {
@override
double progress() => 1.0;
@override
String toString() => 'Reconnecting';
}

View file

@ -16,5 +16,6 @@ class Unspent {
bool isFrozen;
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`
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:
`$ flutter packages pub run build_runner build --delete-conflicting-outputs`
`$ ./model_generator.sh`
### 9. Build!

View file

@ -188,4 +188,9 @@ class CWBitcoin extends Bitcoin {
@override
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}\$'
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
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:
return '[0-9a-zA-Z_]';
case CryptoCurrency.banano:
@ -274,7 +274,8 @@ class AddressValidator extends TextValidator {
'([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)'
'([^0-9a-zA-Z]|^)${P2wpkhAddress.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:
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]|\$)'

View file

@ -3,7 +3,9 @@ import 'package:cw_core/sync_status.dart';
String syncStatusTitle(SyncStatus syncStatus) {
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) {

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:cw_core/crypto_currency.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 'core/totp_request_details.dart';
@ -657,6 +658,10 @@ Future<void> setup({
getIt.registerFactory<MoneroAccountListViewModel>(() {
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) {
return MoneroAccountListViewModel(wallet);
}

View file

@ -10,8 +10,7 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:cake_wallet/generated/i18n.dart';
class PresentReceiveOptionPicker extends StatelessWidget {
PresentReceiveOptionPicker(
{required this.receiveOptionViewModel, required this.color});
PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color});
final ReceiveOptionViewModel receiveOptionViewModel;
final Color color;
@ -43,17 +42,11 @@ class PresentReceiveOptionPicker extends StatelessWidget {
Text(
S.current.receive,
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
fontFamily: 'Lato',
color: color),
fontSize: 18.0, fontWeight: FontWeight.bold, fontFamily: 'Lato', color: color),
),
Observer(
builder: (_) => Text(receiveOptionViewModel.selectedReceiveOption.toString(),
style: TextStyle(
fontSize: 10.0,
fontWeight: FontWeight.w500,
color: color)))
builder: (_) => Text(describeOption(receiveOptionViewModel.selectedReceiveOption),
style: TextStyle(fontSize: 10.0, fontWeight: FontWeight.w500, color: color)))
],
),
SizedBox(width: 5),
@ -73,7 +66,8 @@ class PresentReceiveOptionPicker extends StatelessWidget {
backgroundColor: Colors.transparent,
body: Stack(
alignment: AlignmentDirectional.center,
children:[ AlertBackground(
children: [
AlertBackground(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
@ -107,10 +101,12 @@ class PresentReceiveOptionPicker extends StatelessWidget {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(option.toString(),
Text(describeOption(option),
textAlign: TextAlign.left,
style: textSmall(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.titleColor,
).copyWith(
fontWeight:
value == option ? FontWeight.w800 : FontWeight.w500,

View file

@ -67,8 +67,7 @@ class ReceivePage extends BasePage {
@override
Widget Function(BuildContext, Widget) get rootWrapper =>
(BuildContext context, Widget scaffold) =>
GradientBackground(scaffold: scaffold);
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
@override
Widget trailing(BuildContext context) {
@ -159,7 +158,8 @@ class ReceivePage extends BasePage {
trailingIcon: Icon(
Icons.arrow_forward_ios,
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 =
item.address == addressListViewModel.address.address;
final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
? Theme.of(context)
.extension<ReceivePageTheme>()!
.currentTileBackgroundColor
: Theme.of(context)
.extension<ReceivePageTheme>()!
.tilesBackgroundColor;
final textColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
? Theme.of(context)
.extension<ReceivePageTheme>()!
.currentTileTextColor
: Theme.of(context)
.extension<ReceivePageTheme>()!
.tilesTextColor;
return AddressCell.fromItem(item,
isCurrent: isCurrent,
@ -211,6 +219,16 @@ class ReceivePage extends BasePage {
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/src/screens/exchange/widgets/currency_picker.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/responsive_layout_util.dart';
import 'package:cw_core/crypto_currency.dart';
@ -164,7 +165,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
if (DeviceInfo.instance.isMobile) AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor:

View file

@ -1,5 +1,6 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.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:flutter/services.dart';
import 'package:flutter/material.dart';
@ -133,8 +134,7 @@ class AddressTextField extends StatelessWidget {
),
)),
],
if (this.options.contains(AddressTextFieldOption.qrCode) &&
DeviceInfo.instance.isMobile) ...[
if (this.options.contains(AddressTextFieldOption.qrCode)) ...[
Container(
width: prefixIconWidth,
height: prefixIconHeight,

View file

@ -278,6 +278,11 @@ abstract class DashboardViewModelBase with Store {
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
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;

View file

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

View file

@ -183,8 +183,6 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}) : _baseItems = <ListItem>[],
selectedCurrency = walletTypeToCryptoCurrency(appStore.wallet!.type),
_cryptoNumberFormat = NumberFormat(_cryptoNumberPattern),
hasAccounts =
appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven,
amount = '',
_settingsStore = appStore.settingsStore,
super(appStore: appStore) {
@ -196,7 +194,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
_init();
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';
@ -365,7 +364,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}
@observable
bool hasAccounts;
bool _hasAccounts = false;
@computed
bool get hasAccounts => _hasAccounts;
@computed
String get accountLabel {
@ -380,8 +382,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
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
bool get hasAddressList =>
hasSilentAddresses ||
wallet.type == WalletType.monero ||
wallet.type == WalletType.haven ||
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_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_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_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_solana && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd ..
cd cw_ethereum && flutter pub get && cd ..
cd cw_polygon && flutter pub get && 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_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_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_bitcoin_cash; 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 ..
flutter packages pub run build_runner build --delete-conflicting-outputs

View file

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