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

This commit is contained in:
Rafael Saes 2024-02-20 19:19:25 -03:00
parent d6af37df06
commit e4703a9ace
29 changed files with 1438 additions and 309 deletions

View file

@ -10,21 +10,36 @@ class BitcoinAddressRecord {
int balance = 0, int balance = 0,
String name = '', String name = '',
bool isUsed = false, bool isUsed = false,
required this.type,
String? scriptHash,
required this.network,
this.silentPaymentTweak,
}) : _txCount = txCount, }) : _txCount = txCount,
_balance = balance, _balance = balance,
_name = name, _name = name,
_isUsed = isUsed; _isUsed = isUsed;
factory BitcoinAddressRecord.fromJSON(String jsonSource) { factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(decoded['address'] as String, return BitcoinAddressRecord(
index: decoded['index'] as int, decoded['address'] as String,
isHidden: decoded['isHidden'] as bool? ?? false, index: decoded['index'] as int,
isUsed: decoded['isUsed'] as bool? ?? false, isHidden: decoded['isHidden'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0, isUsed: decoded['isUsed'] as bool? ?? false,
name: decoded['name'] as String? ?? '', txCount: decoded['txCount'] as int? ?? 0,
balance: decoded['balance'] as int? ?? 0); name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
type: decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values
.firstWhere((type) => type.toString() == decoded['type'] as String)
: SegwitAddresType.p2wpkh,
scriptHash: decoded['scriptHash'] as String?,
network: (decoded['network'] as String?) == null
? network
: BasedUtxoNetwork.fromName(decoded['network'] as String),
silentPaymentTweak: decoded['silentPaymentTweak'] as String?,
);
} }
final String address; final String address;
@ -34,6 +49,9 @@ class BitcoinAddressRecord {
int _balance; int _balance;
String _name; String _name;
bool _isUsed; bool _isUsed;
String? scriptHash;
BasedUtxoNetwork? network;
final String? silentPaymentTweak;
int get txCount => _txCount; int get txCount => _txCount;
@ -66,5 +84,9 @@ class BitcoinAddressRecord {
'name': name, 'name': name,
'isUsed': isUsed, 'isUsed': isUsed,
'balance': balance, 'balance': balance,
'type': type.toString(),
'scriptHash': scriptHash,
'network': network?.value,
'silentPaymentTweak': silentPaymentTweak,
}); });
} }

View file

@ -0,0 +1,46 @@
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/receive_page_option.dart';
class BitcoinReceivePageOption implements ReceivePageOption {
static const p2wpkh = BitcoinReceivePageOption._('Segwit (P2WPKH)');
static const p2sh = BitcoinReceivePageOption._('Segwit-Compatible (P2SH)');
static const p2tr = BitcoinReceivePageOption._('Taproot (P2TR)');
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;
String toString() {
return value;
}
static const all = [
BitcoinReceivePageOption.p2wpkh,
BitcoinReceivePageOption.p2sh,
BitcoinReceivePageOption.p2tr,
BitcoinReceivePageOption.p2wsh,
BitcoinReceivePageOption.p2pkh
];
factory BitcoinReceivePageOption.fromType(BitcoinAddressType type) {
switch (type) {
case SegwitAddresType.p2tr:
return BitcoinReceivePageOption.p2tr;
case SegwitAddresType.p2wsh:
return BitcoinReceivePageOption.p2wsh;
case P2pkhAddressType.p2pkh:
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,15 +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( factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) =>
BitcoinAddressRecord address, Map<String, dynamic> json) => BitcoinUnspent(
BitcoinUnspent(address, json['tx_hash'] as String, json['value'] as int, address,
json['tx_pos'] as int); 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

@ -17,17 +17,22 @@ part 'bitcoin_wallet.g.dart';
class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet;
abstract class BitcoinWalletBase extends ElectrumWallet with Store { abstract class BitcoinWalletBase extends ElectrumWallet with Store {
BitcoinWalletBase( BitcoinWalletBase({
{required String mnemonic, required String mnemonic,
required String password, required String password,
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
required Uint8List seedBytes, required Uint8List seedBytes,
List<BitcoinAddressRecord>? initialAddresses, String? addressPageType,
ElectrumBalance? initialBalance, BasedUtxoNetwork? networkParam,
int initialRegularAddressIndex = 0, List<BitcoinAddressRecord>? initialAddresses,
int initialChangeAddressIndex = 0}) ElectrumBalance? initialBalance,
: super( Map<String, int>? initialRegularAddressIndex,
Map<String, int>? initialChangeAddressIndex,
List<BitcoinAddressRecord>? initialSilentAddresses,
int initialSilentAddressIndex = 0,
SilentPaymentOwner? silentAddress,
}) : super(
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
@ -38,15 +43,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
seedBytes: seedBytes, seedBytes: seedBytes,
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,
mainHd: hd, initialSilentAddressIndex: initialSilentAddressIndex,
sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) silentAddress: silentAddress,
.derivePath("m/0'/1"), mainHd: hd,
networkType: networkType); sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType).derivePath("m/0'/1"),
network: networkParam ?? network,
);
autorun((_) { autorun((_) {
this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress;
}); });
@ -58,20 +65,29 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
required WalletInfo walletInfo, required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
List<BitcoinAddressRecord>? initialSilentAddresses,
ElectrumBalance? initialBalance, ElectrumBalance? initialBalance,
int initialRegularAddressIndex = 0, Map<String, int>? initialRegularAddressIndex,
int initialChangeAddressIndex = 0 Map<String, int>? initialChangeAddressIndex,
int initialSilentAddressIndex = 0,
}) async { }) async {
return BitcoinWallet( return BitcoinWallet(
mnemonic: mnemonic, mnemonic: mnemonic,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: initialAddresses, initialAddresses: initialAddresses,
initialBalance: initialBalance, initialSilentAddresses: initialSilentAddresses,
seedBytes: await mnemonicToSeedBytes(mnemonic), initialSilentAddressIndex: initialSilentAddressIndex,
initialRegularAddressIndex: initialRegularAddressIndex, silentAddress: await SilentPaymentOwner.fromMnemonic(mnemonic,
initialChangeAddressIndex: initialChangeAddressIndex); hrp: network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
initialBalance: initialBalance,
seedBytes: await mnemonicToSeedBytes(mnemonic),
initialRegularAddressIndex: initialRegularAddressIndex,
initialChangeAddressIndex: initialChangeAddressIndex,
addressPageType: addressPageType,
networkParam: network,
);
} }
static Future<BitcoinWallet> open({ static Future<BitcoinWallet> open({
@ -82,14 +98,21 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}) async { }) async {
final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password); final snp = await ElectrumWallletSnapshot.load(name, walletInfo.type, password);
return BitcoinWallet( return BitcoinWallet(
mnemonic: snp.mnemonic, mnemonic: snp.mnemonic,
password: password, password: password,
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialAddresses: snp.addresses, initialAddresses: snp.addresses,
initialBalance: snp.balance, initialSilentAddresses: snp.silentAddresses,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic), initialSilentAddressIndex: snp.silentAddressIndex,
initialRegularAddressIndex: snp.regularAddressIndex, silentAddress: await SilentPaymentOwner.fromMnemonic(snp.mnemonic,
initialChangeAddressIndex: snp.changeAddressIndex); hrp: snp.network == BitcoinNetwork.testnet ? 'tsp' : 'sp'),
initialBalance: snp.balance,
seedBytes: await mnemonicToSeedBytes(snp.mnemonic),
initialRegularAddressIndex: snp.regularAddressIndex,
initialChangeAddressIndex: snp.changeAddressIndex,
addressPageType: snp.addressPageType,
networkParam: snp.network,
);
} }
} }

View file

@ -11,22 +11,18 @@ part 'bitcoin_wallet_addresses.g.dart';
class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses; class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses;
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store { abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
BitcoinWalletAddressesBase(WalletInfo walletInfo, BitcoinWalletAddressesBase(
{required bitcoin.HDWallet mainHd, WalletInfo walletInfo, {
required bitcoin.HDWallet sideHd, required super.mainHd,
required bitcoin.NetworkType networkType, required super.sideHd,
required ElectrumClient electrumClient, required super.network,
List<BitcoinAddressRecord>? initialAddresses, super.initialAddresses,
int initialRegularAddressIndex = 0, super.initialRegularAddressIndex,
int initialChangeAddressIndex = 0}) super.initialChangeAddressIndex,
: super(walletInfo, super.initialSilentAddresses,
initialAddresses: initialAddresses, super.initialSilentAddressIndex = 0,
initialRegularAddressIndex: initialRegularAddressIndex, super.silentAddress,
initialChangeAddressIndex: initialChangeAddressIndex, }) : super(walletInfo);
mainHd: mainHd,
sideHd: sideHd,
electrumClient: electrumClient,
networkType: networkType);
@override @override
String getAddress({required int index, required bitcoin.HDWallet hd}) => String getAddress({required int index, required bitcoin.HDWallet hd}) =>

View file

@ -335,6 +335,27 @@ class ElectrumClient {
} }
} }
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
// example response:
// {
// "height": 520481,
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// }
Future<int?> getCurrentBlockChainTip() =>
call(method: 'blockchain.headers.subscribe').then((result) {
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
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,8 +1,8 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_base/bitcoin_base.dart';
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 +20,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 +30,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 +41,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( factory ElectrumTransactionInfo.fromElectrumVerbose(
@ -143,52 +148,24 @@ class ElectrumTransactionInfo extends TransactionInfo {
confirmations: bundle.confirmations); confirmations: bundle.confirmations);
} }
factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex, factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
{List<String>? addresses, required int height, int? timestamp, required int confirmations}) { return ElectrumTransactionInfo(
final tx = bitcoin.Transaction.fromHex(hex); type,
var exist = false; id: data['id'] as String,
var amount = 0; height: data['height'] as int,
amount: data['amount'] as int,
if (addresses != null) { fee: data['fee'] as int,
tx.outs.forEach((out) { direction: parseTransactionDirectionFromInt(data['direction'] as int),
try { date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
final p2pkh = bitcoin.P2PKH( isPending: data['isPending'] as bool,
data: PaymentData(output: out.script), network: bitcoin.bitcoin); confirmations: data['confirmations'] as int,
exist = addresses.contains(p2pkh.data.address); to: data['to'] as String?,
unspent: data['unspent'] != null
if (exist) { ? BitcoinUnspent.fromJSON(
amount += out.value!; BitcoinAddressRecord.fromJSON(data['unspent']['address_record'] as String),
} data['unspent'] as Map<String, dynamic>)
} catch (_) {} : null,
}); );
}
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,
id: data['id'] as String,
height: data['height'] as int,
amount: data['amount'] as int,
fee: data['fee'] as int,
direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int);
} }
final WalletType type; final WalletType type;
@ -232,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_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
@ -130,22 +131,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();
await walletAddresses.discoverAddresses(); } catch (_) {}
try {
rescan(height: walletInfo.restoreHeight);
await updateTransactions(); await updateTransactions();
_subscribeForUpdates(); _subscribeForUpdates();
await updateUnspent(); await updateUnspent();
@ -175,6 +247,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();
@ -197,7 +275,47 @@ abstract class ElectrumWalletBase
for (final utx in unspentCoins) { for (final utx in unspentCoins) {
if (utx.isSending) { if (utx.isSending) {
allInputsAmount += utx.value; allInputsAmount += utx.value;
inputs.add(utx); 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;
}
} }
} }
@ -344,20 +462,83 @@ abstract class ElectrumWalletBase
txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue);
} }
return PendingBitcoinTransaction(txb.build(), type, if (SilentPaymentAddress.regex.hasMatch(outputAddress)) {
electrumClient: electrumClient, amount: amount, fee: fee) // final outpointsHash = SilentPayment.hashOutpoints(outpoints);
..addListener((transaction) async { // final generatedOutputs = SilentPayment.generateMultipleRecipientPubkeys(inputPrivKeys,
transactionHistory.addOne(transaction); // outpointsHash, SilentPaymentDestination.fromAddress(outputAddress, outputAmount!));
await updateBalance();
// 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),
// ));
// });
// });
}
outputAddresses.add(address);
if (hasMultiDestination) {
if (out.sendAll || out.formattedCryptoAmount! <= 0) {
throw BitcoinTransactionWrongBalanceException(currency);
}
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
} else {
if (!sendAll) {
final outputAmount = out.formattedCryptoAmount!;
credentialsAmount += outputAmount;
outputs.add(BitcoinOutput(address: address, value: BigInt.from(outputAmount)));
} else {
// The value will be changed after estimating the Tx size and deducting the fee from the total
outputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
}
}
}
final estimatedTx = await _estimateTxFeeAndInputsToUse(
credentialsAmount, sendAll, outputAddresses, outputs, transactionCredentials);
final txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network);
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key = estimatedTx.privateKeys
.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
if (key == null) {
throw Exception("Cannot find private key");
}
if (utxo.utxo.isP2tr()) {
return key.signTapRoot(txDigest, sighash: sighash);
} else {
return key.signInput(txDigest, sigHash: sighash);
}
}); });
} }
String toJSON() => json.encode({ String toJSON() => json.encode({
'mnemonic': mnemonic, 'mnemonic': mnemonic,
'account_index': walletAddresses.currentReceiveAddressIndex.toString(), 'account_index': walletAddresses.currentReceiveAddressIndexByType,
'change_address_index': walletAddresses.currentChangeAddressIndex.toString(), 'change_address_index': walletAddresses.currentChangeAddressIndexByType,
'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
'balance': balance[currency]?.toJSON() 'address_page_type': walletInfo.addressPageType == null
? 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',
}); });
int feeRate(TransactionPriority priority) { int feeRate(TransactionPriority priority) {
@ -461,21 +642,38 @@ abstract class ElectrumWalletBase
generateKeyPair(hd: hd, index: index, network: networkType); generateKeyPair(hd: hd, index: index, network: networkType);
@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 {
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient // Update unspents stored from scanned silent payment transactions
.getListUnspentWithAddress(address.address, networkType) transactionHistory.transactions.values.forEach((tx) {
.then((unspent) => unspent.map((unspent) { 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();
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, network)
.then((unspent) => Future.forEach<Map<String, dynamic>>(unspent, (unspent) async {
try { try {
return BitcoinUnspent.fromJSON(address, unspent); return BitcoinUnspent.fromJSON(address, unspent);
} catch (_) { } catch (_) {
@ -495,8 +693,10 @@ abstract class ElectrumWalletBase
if (unspentCoins.isNotEmpty) { if (unspentCoins.isNotEmpty) {
unspentCoins.forEach((coin) { unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values final coinInfoList = unspentCoinsInfo.values.where((element) =>
.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash)); element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.address.contains(coin.address));
if (coinInfoList.isNotEmpty) { if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first; final coinInfo = coinInfoList.first;
@ -513,6 +713,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,
@ -569,7 +770,22 @@ abstract class ElectrumWalletBase
ins.add(tx); ins.add(tx);
} }
return ElectrumTransactionBundle(original, ins: ins, time: time, confirmations: confirmations); 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 = BtcTransaction.fromRaw(txHex);
ins.add(tx);
} catch (_) {
ins.add(BtcTransaction.fromRaw(await electrumClient.getTransactionHex(hash: vin.txId)));
}
}
return ElectrumTransactionBundle(original,
ins: ins, time: time, confirmations: confirmations, height: height);
} }
Future<ElectrumTransactionInfo?> fetchTransactionInfo( Future<ElectrumTransactionInfo?> fetchTransactionInfo(
@ -666,7 +882,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);
@ -685,6 +901,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 {
@ -698,20 +931,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 &&
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];
@ -758,4 +996,426 @@ 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;
}
} }

View file

@ -1,8 +1,6 @@
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
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_bitcoin/script_hash.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';
@ -13,24 +11,39 @@ part 'electrum_wallet_addresses.g.dart';
class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses;
abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(WalletInfo walletInfo, ElectrumWalletAddressesBase(
{required this.mainHd, WalletInfo walletInfo, {
required this.sideHd, required this.mainHd,
required this.electrumClient, required this.sideHd,
required this.networkType, required this.network,
List<BitcoinAddressRecord>? initialAddresses, List<BitcoinAddressRecord>? initialAddresses,
int initialRegularAddressIndex = 0, Map<String, int>? initialRegularAddressIndex,
int initialChangeAddressIndex = 0}) Map<String, int>? initialChangeAddressIndex,
: addresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []).toSet()), 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 ?? []) receiveAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? []) changeAddresses = ObservableList<BitcoinAddressRecord>.of((initialAddresses ?? [])
.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed)
.toSet()), .toSet()),
currentReceiveAddressIndex = initialRegularAddressIndex, currentReceiveAddressIndexByType = initialRegularAddressIndex ?? {},
currentChangeAddressIndex = initialChangeAddressIndex, currentChangeAddressIndexByType = initialChangeAddressIndex ?? {},
super(walletInfo); _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();
}
static const defaultReceiveAddressesCount = 22; static const defaultReceiveAddressesCount = 22;
static const defaultChangeAddressesCount = 17; static const defaultChangeAddressesCount = 17;
@ -43,18 +56,52 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
final ObservableList<BitcoinAddressRecord> addresses; final ObservableList<BitcoinAddressRecord> addresses;
final ObservableList<BitcoinAddressRecord> receiveAddresses; final ObservableList<BitcoinAddressRecord> receiveAddresses;
final ObservableList<BitcoinAddressRecord> changeAddresses; final ObservableList<BitcoinAddressRecord> changeAddresses;
final ElectrumClient electrumClient; final ObservableList<BitcoinAddressRecord> silentAddresses;
final bitcoin.NetworkType networkType; final BasedUtxoNetwork network;
final bitcoin.HDWallet mainHd; final bitcoin.HDWallet mainHd;
final bitcoin.HDWallet sideHd; 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 @override
@computed @computed
String get address { String get address {
if (isEnabledAutoGenerateSubaddress) { if (addressPageType == SilentPaymentsAddresType.p2sp) {
if (receiveAddresses.isEmpty) { if (activeSilentAddress != null) {
final newAddress = generateNewAddress(hd: mainHd).address; return activeSilentAddress!;
return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress; }
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 receiveAddress = receiveAddresses.first.address;
@ -78,6 +125,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);
} }
@ -94,6 +146,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
int currentReceiveAddressIndex; int currentReceiveAddressIndex;
int currentChangeAddressIndex; int currentChangeAddressIndex;
int currentSilentAddressIndex;
@observable @observable
BitcoinAddressRecord? previousAddressRecord; BitcoinAddressRecord? previousAddressRecord;
@ -157,8 +211,45 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
return address; return address;
} }
BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) { Map<String, String> get labels {
final isHidden = hd == sideHd; 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);
final newAddressIndex = addresses.fold( final newAddressIndex = addresses.fold(
0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc); 0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc);

View file

@ -12,9 +12,14 @@ class ElectrumWallletSnapshot {
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.network,
required this.silentAddressIndex,
});
final String name; final String name;
final String password; final String password;
@ -22,29 +27,52 @@ class ElectrumWallletSnapshot {
String mnemonic; String mnemonic;
List<BitcoinAddressRecord> addresses; List<BitcoinAddressRecord> addresses;
List<BitcoinAddressRecord> silentAddresses;
ElectrumBalance balance; ElectrumBalance balance;
int regularAddressIndex; Map<String, int> regularAddressIndex;
int changeAddressIndex; Map<String, int> changeAddressIndex;
int silentAddressIndex;
static Future<ElectrumWallletSnapshot> load(String name, WalletType type, String password) 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)) .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 regularAddressIndex = 0; var regularAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
var changeAddressIndex = 0; var changeAddressIndexByType = {SegwitAddresType.p2wpkh.toString(): 0};
var silentAddressIndex = 0;
try { try {
regularAddressIndex = int.parse(data['account_index'] as String? ?? '0'); regularAddressIndexByType = {
changeAddressIndex = int.parse(data['change_address_index'] as String? ?? '0'); SegwitAddresType.p2wpkh.toString(): int.parse(data['account_index'] as String? ?? '0')
} catch (_) {} };
changeAddressIndexByType = {
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>? ?? {};
changeAddressIndexByType = data["change_address_index"] as Map<String, int>? ?? {};
} catch (_) {}
}
return ElectrumWallletSnapshot( return ElectrumWallletSnapshot(
name: name, name: name,
@ -52,8 +80,13 @@ class ElectrumWallletSnapshot {
password: password, password: password,
mnemonic: mnemonic, mnemonic: mnemonic,
addresses: addresses, addresses: addresses,
silentAddresses: silentAddresses,
balance: balance, balance: balance,
regularAddressIndex: regularAddressIndex, regularAddressIndex: regularAddressIndexByType,
changeAddressIndex: changeAddressIndex); 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

@ -75,6 +75,15 @@ packages:
url: "https://github.com/cake-tech/bitbox-flutter.git" url: "https://github.com/cake-tech/bitbox-flutter.git"
source: git source: git
version: "1.0.1" version: "1.0.1"
bitcoin_base:
dependency: "direct main"
description:
path: "."
ref: cake-update-v2
resolved-ref: e4686da77cace5400697de69f7885020297cb900
url: "https://github.com/cake-tech/bitcoin_base.git"
source: git
version: "4.0.0"
bitcoin_flutter: bitcoin_flutter:
dependency: "direct main" dependency: "direct main"
description: description:
@ -84,6 +93,15 @@ 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: transitive
description:
path: "."
ref: cake-update-v1
resolved-ref: b4db705d5409aba30730818ed0d3c09aac5b9992
url: "https://github.com/rafael-xmr/blockchain_utils"
source: git
version: "1.6.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -30,6 +30,10 @@ dependencies:
rxdart: ^0.27.5 rxdart: ^0.27.5
unorm_dart: ^0.2.0 unorm_dart: ^0.2.0
cryptography: ^2.0.5 cryptography: ^2.0.5
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base.git
ref: cake-update-v2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -29,7 +29,10 @@ dependencies:
git: git:
url: https://github.com/cake-tech/bitbox-flutter.git url: https://github.com/cake-tech/bitbox-flutter.git
ref: master ref: master
bitcoin_base: ^3.0.1 bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base.git
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

@ -176,6 +176,173 @@ class CWBitcoin extends Bitcoin {
=> BitcoinTransactionPriority.slow; => BitcoinTransactionPriority.slow;
@override @override
TransactionPriority getLitecoinTransactionPrioritySlow() List<String> getWordList() => wordlist;
=> LitecoinTransactionPriority.slow;
} @override
Map<String, String> getWalletKeys(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys;
return <String, String>{
'wif': keys.wif,
'privateKey': keys.privateKey,
'publicKey': keys.publicKey
};
}
@override
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
@override
List<TransactionPriority> getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all;
@override
TransactionPriority deserializeBitcoinTransactionPriority(int raw) =>
BitcoinTransactionPriority.deserialize(raw: raw);
@override
TransactionPriority deserializeLitecoinTransactionPriority(int raw) =>
LitecoinTransactionPriority.deserialize(raw: raw);
@override
int getFeeRate(Object wallet, TransactionPriority priority) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeRate(priority);
}
@override
Future<void> generateNewAddress(Object wallet, String label) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.walletAddresses.generateNewAddress(label: label);
await wallet.save();
}
@override
Future<void> updateAddress(Object wallet, String address, String label) async {
final bitcoinWallet = wallet as ElectrumWallet;
bitcoinWallet.walletAddresses.updateAddress(address, label);
await wallet.save();
}
@override
Object createBitcoinTransactionCredentials(List<Output> outputs,
{required TransactionPriority priority, int? feeRate}) =>
BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: feeRate);
@override
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs,
{TransactionPriority? priority, required int feeRate}) =>
BitcoinTransactionCredentials(outputs,
priority: priority != null ? priority as BitcoinTransactionPriority : null,
feeRate: feeRate);
@override
List<String> getAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.addressesByReceiveType
.map((BitcoinAddressRecord addr) => addr.address)
.toList();
}
@override
@computed
List<ElectrumSubAddress> getSubAddresses(Object wallet) {
final electrumWallet = wallet as ElectrumWallet;
return electrumWallet.walletAddresses.addressesByReceiveType
.map((BitcoinAddressRecord addr) => ElectrumSubAddress(
id: addr.index,
name: addr.name,
address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address,
txCount: addr.txCount,
balance: addr.balance,
isChange: addr.isHidden))
.toList();
}
@override
String getAddress(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.address;
}
@override
String formatterBitcoinAmountToString({required int amount}) =>
bitcoinAmountToString(amount: amount);
@override
double formatterBitcoinAmountToDouble({required int amount}) =>
bitcoinAmountToDouble(amount: amount);
@override
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
@override
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) =>
(priority as BitcoinTransactionPriority).labelWithRate(rate);
@override
List<BitcoinUnspent> getUnspents(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.unspentCoins;
}
Future<void> updateUnspents(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateUnspent();
}
WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource);
}
WalletService createLitecoinWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource) {
return LitecoinWalletService(walletInfoSource, unspentCoinSource);
}
@override
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
@override
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
@override
TransactionPriority getBitcoinTransactionPrioritySlow() => BitcoinTransactionPriority.slow;
@override
TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow;
@override
Future<void> setAddressType(Object wallet, dynamic option) async {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.walletAddresses.setAddressType(option as BitcoinAddressType);
}
@override
BitcoinReceivePageOption getSelectedAddressType(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return BitcoinReceivePageOption.fromType(bitcoinWallet.walletAddresses.addressPageType);
}
@override
List<BitcoinReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
List<BitcoinAddressRecord> getSilentAddresses(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.walletAddresses.silentAddresses;
}
}

View file

@ -25,7 +25,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 '^3[0-9a-zA-Z]{32}\$|^3[0-9a-zA-Z]{33}\$|^bc1[0-9a-zA-Z]{59}\$'; 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:
@ -263,12 +263,12 @@ class AddressValidator extends TextValidator {
'|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.btc: case CryptoCurrency.btc:
return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' return '([^0-9a-zA-Z]|^)${P2pkhAddress.regex.pattern}|\$)'
'|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' '([^0-9a-zA-Z]|^)${P2shAddress.regex.pattern}|\$)'
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)' '([^0-9a-zA-Z]|^)${P2wpkhAddress.regex.pattern}|\$)'
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)' '([^0-9a-zA-Z]|^)${P2wshAddress.regex.pattern}|\$)'
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)' '([^0-9a-zA-Z]|^)${P2trAddress.regex.pattern}|\$)'
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)'; '|${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]|\$)'
@ -290,4 +290,4 @@ class AddressValidator extends TextValidator {
return null; return null;
} }
} }
} }

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

@ -228,6 +228,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';
@ -656,6 +657,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

@ -274,6 +274,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

@ -167,8 +167,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) {
@ -180,7 +178,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';
@ -339,7 +338,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 {
@ -354,8 +356,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,10 +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_ethereum && flutter pub get && cd .. cd cw_polygon; flutter pub get; 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 flutter packages pub run build_runner build --delete-conflicting-outputs

View file

@ -118,6 +118,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);