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,
String name = '',
bool isUsed = false,
required this.type,
String? scriptHash,
required this.network,
this.silentPaymentTweak,
}) : _txCount = txCount,
_balance = balance,
_name = name,
_isUsed = isUsed;
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
factory BitcoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map;
return BitcoinAddressRecord(decoded['address'] as String,
index: decoded['index'] as int,
isHidden: decoded['isHidden'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0);
return BitcoinAddressRecord(
decoded['address'] as String,
index: decoded['index'] as int,
isHidden: decoded['isHidden'] as bool? ?? false,
isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] 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;
@ -34,6 +49,9 @@ class BitcoinAddressRecord {
int _balance;
String _name;
bool _isUsed;
String? scriptHash;
BasedUtxoNetwork? network;
final String? silentPaymentTweak;
int get txCount => _txCount;
@ -66,5 +84,9 @@ class BitcoinAddressRecord {
'name': name,
'isUsed': isUsed,
'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_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);
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,
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

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

View file

@ -1,8 +1,8 @@
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:bitcoin_base/bitcoin_base.dart';
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 +20,8 @@ class ElectrumTransactionBundle {
}
class ElectrumTransactionInfo extends TransactionInfo {
BitcoinUnspent? unspent;
ElectrumTransactionInfo(this.type,
{required String id,
required int height,
@ -28,7 +30,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 +41,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.date = date;
this.isPending = isPending;
this.confirmations = confirmations;
this.to = to;
}
factory ElectrumTransactionInfo.fromElectrumVerbose(
@ -143,52 +148,24 @@ 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,
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);
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,
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;
@ -232,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_flutter/bitcoin_flutter.dart' as bitcoin;
@ -130,22 +131,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 walletAddresses.discoverAddresses();
await _setInitialHeight();
} catch (_) {}
try {
rescan(height: walletInfo.restoreHeight);
await updateTransactions();
_subscribeForUpdates();
await updateUnspent();
@ -175,6 +247,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();
@ -197,7 +275,47 @@ abstract class ElectrumWalletBase
for (final utx in unspentCoins) {
if (utx.isSending) {
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);
}
return PendingBitcoinTransaction(txb.build(), type,
electrumClient: electrumClient, amount: amount, fee: fee)
..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
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),
// ));
// });
// });
}
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({
'mnemonic': mnemonic,
'account_index': walletAddresses.currentReceiveAddressIndex.toString(),
'change_address_index': walletAddresses.currentChangeAddressIndex.toString(),
'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(),
'balance': balance[currency]?.toJSON()
'account_index': walletAddresses.currentReceiveAddressIndexByType,
'change_address_index': walletAddresses.currentChangeAddressIndexByType,
'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(),
'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) {
@ -461,21 +642,38 @@ abstract class ElectrumWalletBase
generateKeyPair(hd: hd, index: index, network: networkType);
@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 {
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, networkType)
.then((unspent) => unspent.map((unspent) {
// 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();
await Future.wait(walletAddresses.allAddresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, network)
.then((unspent) => Future.forEach<Map<String, dynamic>>(unspent, (unspent) async {
try {
return BitcoinUnspent.fromJSON(address, unspent);
} catch (_) {
@ -495,8 +693,10 @@ abstract class ElectrumWalletBase
if (unspentCoins.isNotEmpty) {
unspentCoins.forEach((coin) {
final coinInfoList = unspentCoinsInfo.values
.where((element) => element.walletId.contains(id) && element.hash.contains(coin.hash));
final coinInfoList = unspentCoinsInfo.values.where((element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.address.contains(coin.address));
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
@ -513,6 +713,7 @@ abstract class ElectrumWalletBase
await _refreshUnspentCoinsInfo();
}
@action
Future<void> _addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo(
walletId: id,
@ -569,7 +770,22 @@ abstract class ElectrumWalletBase
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(
@ -666,7 +882,7 @@ abstract class ElectrumWalletBase
}
}
void _subscribeForUpdates() {
void _subscribeForUpdates() async {
scriptHashes.forEach((sh) async {
await _scripthashesUpdateSubject[sh]?.close();
_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 {
@ -698,20 +931,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 &&
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];
@ -758,4 +996,426 @@ 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;
}
}

View file

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

View file

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

@ -75,6 +75,15 @@ packages:
url: "https://github.com/cake-tech/bitbox-flutter.git"
source: git
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:
dependency: "direct main"
description:
@ -84,6 +93,15 @@ packages:
url: "https://github.com/cake-tech/bitcoin_flutter.git"
source: git
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:
dependency: transitive
description:

View file

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

View file

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

@ -176,6 +176,173 @@ class CWBitcoin extends Bitcoin {
=> BitcoinTransactionPriority.slow;
@override
TransactionPriority getLitecoinTransactionPrioritySlow()
=> LitecoinTransactionPriority.slow;
}
List<String> getWordList() => wordlist;
@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}\$'
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
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:
return '[0-9a-zA-Z_]';
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]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)';
case CryptoCurrency.btc:
return '([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)1[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{32}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)3[0-9a-zA-Z]{33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{39}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)bc1[0-9a-zA-Z]{59}([^0-9a-zA-Z]|\$)';
return '([^0-9a-zA-Z]|^)${P2pkhAddress.regex.pattern}|\$)'
'([^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}|\$)'
'|${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]|\$)'
@ -290,4 +290,4 @@ class AddressValidator extends TextValidator {
return null;
}
}
}
}

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) {
@ -35,4 +37,4 @@ String syncStatusTitle(SyncStatus syncStatus) {
}
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: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';
@ -656,6 +657,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,65 +66,68 @@ class PresentReceiveOptionPicker extends StatelessWidget {
backgroundColor: Colors.transparent,
body: Stack(
alignment: AlignmentDirectional.center,
children:[ AlertBackground(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(),
Container(
margin: EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.background,
),
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 24),
child: (ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: receiveOptionViewModel.options.length,
itemBuilder: (_, index) {
final option = receiveOptionViewModel.options[index];
return InkWell(
onTap: () {
Navigator.pop(popUpContext);
children: [
AlertBackground(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Spacer(),
Container(
margin: EdgeInsets.symmetric(horizontal: 24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.background,
),
child: Padding(
padding: const EdgeInsets.only(top: 24, bottom: 24),
child: (ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: receiveOptionViewModel.options.length,
itemBuilder: (_, index) {
final option = receiveOptionViewModel.options[index];
return InkWell(
onTap: () {
Navigator.pop(popUpContext);
receiveOptionViewModel.selectReceiveOption(option);
},
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Observer(builder: (_) {
final value = receiveOptionViewModel.selectedReceiveOption;
receiveOptionViewModel.selectReceiveOption(option);
},
child: Padding(
padding: const EdgeInsets.only(left: 24, right: 24),
child: Observer(builder: (_) {
final value = receiveOptionViewModel.selectedReceiveOption;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(option.toString(),
textAlign: TextAlign.left,
style: textSmall(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
).copyWith(
fontWeight:
value == option ? FontWeight.w800 : FontWeight.w500,
)),
RoundedCheckbox(
value: value == option,
)
],
);
}),
),
);
},
separatorBuilder: (_, index) => SizedBox(height: 30),
)),
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(describeOption(option),
textAlign: TextAlign.left,
style: textSmall(
color: Theme.of(context)
.extension<CakeTextTheme>()!
.titleColor,
).copyWith(
fontWeight:
value == option ? FontWeight.w800 : FontWeight.w500,
)),
RoundedCheckbox(
value: value == option,
)
],
);
}),
),
);
},
separatorBuilder: (_, index) => SizedBox(height: 30),
)),
),
),
),
Spacer()
],
Spacer()
],
),
),
),
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
],
),

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,
@ -194,7 +194,7 @@ class AddressTextField extends StatelessWidget {
Future<void> _presentQRScanner(BuildContext context) async {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
final code = await presentQRScanner();
if (code.isEmpty) {

View file

@ -274,6 +274,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';
@ -9,8 +10,8 @@ enum RescanWalletState { rescaning, none }
abstract class RescanViewModelBase with Store {
RescanViewModelBase(this._wallet)
: state = RescanWalletState.none,
isButtonEnabled = false;
: state = RescanWalletState.none,
isButtonEnabled = false;
final WalletBase _wallet;
@ -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

@ -167,8 +167,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) {
@ -180,7 +178,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';
@ -339,7 +338,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}
@observable
bool hasAccounts;
bool _hasAccounts = false;
@computed
bool get hasAccounts => _hasAccounts;
@computed
String get accountLabel {
@ -354,8 +356,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,10 +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_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

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