feat: balance and scanning improvements

This commit is contained in:
Rafael Saes 2025-04-23 16:29:40 -03:00
parent 2502a14b50
commit fd4c310088
22 changed files with 655 additions and 558 deletions

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/seedbyte_types.dart'; import 'package:cw_bitcoin/seedbyte_types.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
@ -11,7 +12,7 @@ class BaseBitcoinAddressRecord {
required this.index, required this.index,
bool isChange = false, bool isChange = false,
int txCount = 0, int txCount = 0,
int balance = 0, ElectrumBalance? balance,
String name = '', String name = '',
bool isUsed = false, bool isUsed = false,
required this.type, required this.type,
@ -20,7 +21,7 @@ class BaseBitcoinAddressRecord {
bool? isHidden, bool? isHidden,
String? derivationPath, String? derivationPath,
}) : _txCount = txCount, }) : _txCount = txCount,
_balance = balance, balance = balance ?? ElectrumBalance.zero(),
_name = name, _name = name,
_isUsed = isUsed, _isUsed = isUsed,
isHidden = isHidden ?? isChange, isHidden = isHidden ?? isChange,
@ -38,7 +39,7 @@ class BaseBitcoinAddressRecord {
bool get isChange => _isChange; bool get isChange => _isChange;
final int index; final int index;
int _txCount; int _txCount;
int _balance; ElectrumBalance balance;
String _name; String _name;
bool _isUsed; bool _isUsed;
@ -48,12 +49,8 @@ class BaseBitcoinAddressRecord {
String get name => _name; String get name => _name;
int get balance => _balance;
set txCount(int value) => _txCount = value; set txCount(int value) => _txCount = value;
set balance(int value) => _balance = value;
bool get isUsed => _isUsed; bool get isUsed => _isUsed;
void setAsUsed() { void setAsUsed() {
@ -78,12 +75,12 @@ class BaseBitcoinAddressRecord {
} }
bool getIsUsed() { bool getIsUsed() {
return isUsed || txCount != 0 || balance != 0; return isUsed || txCount != 0 || balance.hasBalance();
} }
// An address not yet used for receiving funds // An address not yet used for receiving funds
bool getIsStillReceiveable(bool autoGenerateAddresses) => bool getIsStillReceiveable(bool autoGenerateAddresses) =>
!autoGenerateAddresses || (!getIsUsed() && !isHidden); !autoGenerateAddresses || (!getIsUsed() && (isChange || !isHidden));
String toJSON() => json.encode({ String toJSON() => json.encode({
'address': address, 'address': address,
@ -94,7 +91,7 @@ class BaseBitcoinAddressRecord {
'isUsed': isUsed, 'isUsed': isUsed,
'txCount': txCount, 'txCount': txCount,
'name': name, 'name': name,
'balance': balance, 'balance': balance.toJSON(),
'type': type.toString(), 'type': type.toString(),
'runtimeType': runtimeType.toString(), 'runtimeType': runtimeType.toString(),
'seedBytesType': seedBytesType?.value, 'seedBytesType': seedBytesType?.value,
@ -115,6 +112,10 @@ class BaseBitcoinAddressRecord {
: SeedBytesType.old_electrum) : SeedBytesType.old_electrum)
: SeedBytesType.fromValue(seedBytesTypeSnp.toString()); : SeedBytesType.fromValue(seedBytesTypeSnp.toString());
final balance = decoded['balance'] is String
? ElectrumBalance.fromJSON(decoded['balance'] as String)
: ElectrumBalance.zero();
return BaseBitcoinAddressRecord( return BaseBitcoinAddressRecord(
decoded['address'] as String, decoded['address'] as String,
network: BasedUtxoNetwork.fromName(decoded['network'] as String), network: BasedUtxoNetwork.fromName(decoded['network'] as String),
@ -125,7 +126,7 @@ class BaseBitcoinAddressRecord {
isUsed: decoded['isUsed'] as bool? ?? false, isUsed: decoded['isUsed'] as bool? ?? false,
txCount: decoded['txCount'] as int? ?? 0, txCount: decoded['txCount'] as int? ?? 0,
name: decoded['name'] as String? ?? '', name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0, balance: balance,
derivationPath: decoded['derivationPath'] as String? ?? '', derivationPath: decoded['derivationPath'] as String? ?? '',
type: decoded['type'] != null && decoded['type'] != '' type: decoded['type'] != null && decoded['type'] != ''
? BitcoinAddressType.values ? BitcoinAddressType.values
@ -140,7 +141,6 @@ class BaseBitcoinAddressRecord {
]) { ]) {
final decoded = json.decode(jsonSource) as Map; final decoded = json.decode(jsonSource) as Map;
final base = buildFromJSON(jsonSource, derivationInfo); final base = buildFromJSON(jsonSource, derivationInfo);
final network = base.network;
if (decoded['runtimeType'] == 'BitcoinAddressRecord') { if (decoded['runtimeType'] == 'BitcoinAddressRecord') {
return BitcoinAddressRecord.fromJSON(jsonSource, base, derivationInfo); return BitcoinAddressRecord.fromJSON(jsonSource, base, derivationInfo);
@ -173,7 +173,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
super.isHidden, super.isHidden,
required super.isChange, required super.isChange,
super.txCount = 0, super.txCount = 0,
super.balance = 0, super.balance,
super.name = '', super.name = '',
super.isUsed = false, super.isUsed = false,
required super.type, required super.type,
@ -279,6 +279,10 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
} }
class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord { class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
static BitcoinDerivationInfo DEFAULT_DERIVATION_INFO =
BitcoinDerivationInfos.SILENT_PAYMENTS_SPEND;
static String DEFAULT_DERIVATION_PATH = DEFAULT_DERIVATION_INFO.derivationPath.toString();
String _derivationPath; String _derivationPath;
@override @override
@ -295,7 +299,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
required super.network, required super.network,
String? derivationPath, String? derivationPath,
super.txCount = 0, super.txCount = 0,
super.balance = 0, super.balance,
super.name = '', super.name = '',
super.isUsed = false, super.isUsed = false,
super.type = SilentPaymentsAddresType.p2sp, super.type = SilentPaymentsAddresType.p2sp,
@ -303,12 +307,16 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
super.seedBytesType, super.seedBytesType,
super.isHidden, super.isHidden,
this.labelHex, this.labelHex,
}) : _derivationPath = derivationPath ?? BitcoinDerivationPaths.SILENT_PAYMENTS_SPEND, }) : _derivationPath = derivationPath ?? DEFAULT_DERIVATION_PATH,
super(index: labelIndex) { super(index: labelIndex) {
if (labelIndex != 0 && labelHex == null) { if (labelIndex != 0 && labelHex == null) {
throw ArgumentError('label must be provided for silent address index != 1'); throw ArgumentError('label must be provided for silent address index != 1');
} }
if (_derivationPath != DEFAULT_DERIVATION_PATH) {
isHidden = true;
}
if (labelIndex != 0 && derivationPath == null) { if (labelIndex != 0 && derivationPath == null) {
_derivationPath = _derivationPath.replaceAll(RegExp(r'\d\/?$'), '$labelIndex'); _derivationPath = _derivationPath.replaceAll(RegExp(r'\d\/?$'), '$labelIndex');
} }
@ -359,7 +367,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
required super.labelIndex, required super.labelIndex,
required super.network, required super.network,
super.txCount = 0, super.txCount = 0,
super.balance = 0, super.balance,
super.name = '', super.name = '',
super.isUsed = false, super.isUsed = false,
required this.tweak, required this.tweak,
@ -434,7 +442,7 @@ class LitecoinMWEBAddressRecord extends BaseBitcoinAddressRecord {
super.isHidden, super.isHidden,
super.isChange = false, super.isChange = false,
super.txCount = 0, super.txCount = 0,
super.balance = 0, super.balance,
super.name = '', super.name = '',
super.isUsed = false, super.isUsed = false,
}) : super(type: SegwitAddressType.mweb) { }) : super(type: SegwitAddressType.mweb) {

View file

@ -3,10 +3,10 @@ import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/seedbyte_types.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/bitcoin_wallet_snapshot.dart'; import 'package:cw_bitcoin/bitcoin_wallet_snapshot.dart';
import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart'; import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_bitcoin/exceptions.dart'; import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
@ -62,23 +62,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
required super.unspentCoinsInfo, required super.unspentCoinsInfo,
required super.encryptionFileUtils, required super.encryptionFileUtils,
required super.hdWallets, required super.hdWallets,
required super.network,
super.mnemonic, super.mnemonic,
super.xpub, super.xpub,
BasedUtxoNetwork? networkParam,
super.initialBalance, super.initialBalance,
super.passphrase, super.passphrase,
bool? alwaysScan,
super.initialUnspentCoins, super.initialUnspentCoins,
bool? alwaysScan,
Map<String, dynamic>? walletAddressesSnapshot, Map<String, dynamic>? walletAddressesSnapshot,
}) : _alwaysScan = alwaysScan ?? false, }) : _alwaysScan = alwaysScan ?? false,
super( super(
network: networkParam == null currency: network == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
? BitcoinNetwork.mainnet
: networkParam == BitcoinNetwork.mainnet
? BitcoinNetwork.mainnet
: BitcoinNetwork.testnet,
currency:
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
) { ) {
if (walletAddressesSnapshot != null) { if (walletAddressesSnapshot != null) {
walletAddresses = BitcoinWalletAddressesBase.fromJson( walletAddresses = BitcoinWalletAddressesBase.fromJson(
@ -91,7 +85,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
} else { } else {
this.walletAddresses = BitcoinWalletAddresses( this.walletAddresses = BitcoinWalletAddresses(
walletInfo, walletInfo,
network: networkParam ?? network, network: network,
isHardwareWallet: isHardwareWallet, isHardwareWallet: isHardwareWallet,
hdWallets: hdWallets, hdWallets: hdWallets,
); );
@ -114,9 +108,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
} }
@override @override
Future<bool?> initAddresses([bool? sync]) async { Future<InitAddressesData> initAddresses([bool? sync]) async {
var isDiscovered = await super.initAddresses(sync); final initData = await super.initAddresses(sync);
bool? discovered; var isDiscovered = initData.isDiscovered;
var discovered = initData.discovered;
// NOTE: will initiate by priority from the first walletAddressTypes // NOTE: will initiate by priority from the first walletAddressTypes
// then proceeds to following ones after got fully discovered response from worker response // then proceeds to following ones after got fully discovered response from worker response
@ -195,11 +190,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
if (isDiscovered == true && sync == false) if (isDiscovered == true && sync == false)
initAddresses(true); initAddresses(true);
else if (isDiscovered == true)
syncStatus = SyncedSyncStatus();
else if (isDiscovered == false && discovered == false) initAddresses(sync); else if (isDiscovered == false && discovered == false) initAddresses(sync);
return isDiscovered; return InitAddressesData(
isDiscovered: isDiscovered,
discovered: discovered,
);
} }
static Future<BitcoinWallet> create({ static Future<BitcoinWallet> create({
@ -211,9 +207,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
String? passphrase, String? passphrase,
BasedUtxoNetwork? network, BasedUtxoNetwork? network,
}) async { }) async {
network = network == null
? BitcoinNetwork.mainnet
: network == BitcoinNetwork.mainnet
? BitcoinNetwork.mainnet
: BitcoinNetwork.testnet;
final hdWallets = await ElectrumWalletBase.getAccountHDWallets( final hdWallets = await ElectrumWalletBase.getAccountHDWallets(
walletInfo: walletInfo, walletInfo: walletInfo,
network: network ?? BitcoinNetwork.mainnet, network: network,
mnemonic: mnemonic, mnemonic: mnemonic,
passphrase: passphrase, passphrase: passphrase,
); );
@ -225,7 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
walletInfo: walletInfo, walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
encryptionFileUtils: encryptionFileUtils, encryptionFileUtils: encryptionFileUtils,
networkParam: network, network: network,
hdWallets: hdWallets, hdWallets: hdWallets,
); );
} }
@ -297,7 +299,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
unspentCoinsInfo: unspentCoinsInfo, unspentCoinsInfo: unspentCoinsInfo,
initialBalance: snp?.balance, initialBalance: snp?.balance,
encryptionFileUtils: encryptionFileUtils, encryptionFileUtils: encryptionFileUtils,
networkParam: network, network: network,
alwaysScan: snp?.alwaysScan, alwaysScan: snp?.alwaysScan,
initialUnspentCoins: snp?.unspentCoins, initialUnspentCoins: snp?.unspentCoins,
walletAddressesSnapshot: snp?.walletAddressesSnapshot, walletAddressesSnapshot: snp?.walletAddressesSnapshot,
@ -531,11 +533,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
walletAddresses.silentPaymentAddresses.forEach((addressRecord) { walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
addressRecord.txCount = 0; addressRecord.txCount = 0;
addressRecord.balance = 0; addressRecord.balance = ElectrumBalance.zero();
}); });
walletAddresses.receivedSPAddresses.forEach((addressRecord) { walletAddresses.receivedSPAddresses.forEach((addressRecord) {
addressRecord.txCount = 0; addressRecord.txCount = 0;
addressRecord.balance = 0; addressRecord.balance = ElectrumBalance.zero();
}); });
final silentPaymentWallet = walletAddresses.silentPaymentWallet; final silentPaymentWallet = walletAddresses.silentPaymentWallet;
@ -560,13 +562,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
walletAddresses.silentPaymentAddresses.forEach((addressRecord) { walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
if (addressRecord.address == silentPaymentAddress.toAddress(network)) { if (addressRecord.address == silentPaymentAddress.toAddress(network)) {
addressRecord.txCount += 1; addressRecord.txCount += 1;
addressRecord.balance += unspent.value; addressRecord.balance.confirmed += unspent.value;
} }
}); });
walletAddresses.receivedSPAddresses.forEach((addressRecord) { walletAddresses.receivedSPAddresses.forEach((addressRecord) {
if (addressRecord.address == receiveAddressRecord.address) { if (addressRecord.address == receiveAddressRecord.address) {
addressRecord.txCount += 1; addressRecord.txCount += 1;
addressRecord.balance += unspent.value; addressRecord.balance.confirmed += unspent.value;
} }
}); });
} }
@ -576,28 +578,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
return null; return null;
} }
@override
void updateCoin(BitcoinUnspent coin) {
final coinInfoList = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.hash.contains(coin.hash) &&
element.vout == coin.vout,
);
if (coinInfoList.isNotEmpty) {
final coinInfo = coinInfoList.first;
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
addCoinInfo(coin);
}
}
@override @override
@action @action
Future<void> addCoinInfo(BitcoinUnspent coin) async { Future<void> addCoinInfo(BitcoinUnspent coin) async {
@ -620,6 +600,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
); );
await unspentCoinsInfo.add(newInfo); await unspentCoinsInfo.add(newInfo);
} else if (coin.address is BitcoinReceivedSPAddressRecord) {
existingCoinInfo.isSilentPayment = true;
await unspentCoinsInfo.add(existingCoinInfo);
} }
} }
@ -671,11 +654,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
if (result.transactions?.isNotEmpty == true) { if (result.transactions?.isNotEmpty == true) {
walletAddresses.silentPaymentAddresses.forEach((addressRecord) { walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
addressRecord.txCount = 0; addressRecord.txCount = 0;
addressRecord.balance = 0; addressRecord.balance = ElectrumBalance.zero();
}); });
walletAddresses.receivedSPAddresses.forEach((addressRecord) { walletAddresses.receivedSPAddresses.forEach((addressRecord) {
addressRecord.txCount = 0; addressRecord.txCount = 0;
addressRecord.balance = 0; addressRecord.balance = ElectrumBalance.zero();
}); });
for (final map in result.transactions!.entries) { for (final map in result.transactions!.entries) {
@ -693,7 +676,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
existingTxInfo.amount = tx.amount; existingTxInfo.amount = tx.amount;
existingTxInfo.confirmations = tx.confirmations; existingTxInfo.confirmations = tx.confirmations;
existingTxInfo.height = tx.height; existingTxInfo.height = tx.height;
existingTxInfo.direction = TransactionDirection.incoming;
existingTxInfo.inputAddresses = tx.inputAddresses;
existingTxInfo.outputAddresses = tx.outputAddresses;
existingTxInfo.isReceivedSilentPayment = tx.isReceivedSilentPayment;
// TODO: send from tweaks a histories, unspent and amount response
final newUnspents = unspents final newUnspents = unspents
.where( .where(
(unspent) => !unspentCoins.any((element) => (unspent) => !unspentCoins.any((element) =>
@ -710,29 +698,27 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
unspentCoins.forEach(updateCoin); unspentCoins.forEach(updateCoin);
await refreshUnspentCoinsInfo(); await refreshUnspentCoinsInfo();
final newAmount = newUnspents.length > 1
? newUnspents.map((e) => e.value).reduce((value, unspent) => value + unspent)
: newUnspents[0].value;
if (existingTxInfo.direction == TransactionDirection.incoming) {
existingTxInfo.amount += newAmount;
} }
// Updates existing TX // Updates existing TX
transactionHistory.addOne(existingTxInfo); transactionHistory.addOne(existingTxInfo);
// Update balance record
balance[currency]!.confirmed += newAmount;
}
} else { } else {
// else: First time seeing this TX after scanning // else: First time seeing this TX after scanning
unspentCoins.forEach(_updateSilentAddressRecord); unspentCoins.forEach(_updateSilentAddressRecord);
transactionHistory.addOne(tx); transactionHistory.addOne(tx);
balance[currency]!.confirmed += tx.amount;
} }
await updateAllUnspents(); // Update balance record
for (final addressRecord in walletAddresses.otherAddresses) {
final addressBalance = addressRecord.balance;
if (addressBalance.hasBalance()) {
balance[currency]!.confirmed += addressBalance.confirmed;
}
}
await save();
} }
} }
} }
@ -933,13 +919,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
isSilentPayment = true; isSilentPayment = true;
} else if (!isHardwareWallet) { } else if (!isHardwareWallet) {
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord); final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
final path = addressRecord.derivationInfo.derivationPath
.addElem(Bip32KeyIndex(
BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange),
))
.addElem(Bip32KeyIndex(addressRecord.index));
privkey = ECPrivate.fromBip32(bip32: hdWallet.derive(path)); privkey = ECPrivate.fromBip32(
bip32: hdWallets[addressRecord.seedBytesType]!.derive(
Bip32PathParser.parse(addressRecord.indexedDerivationPath),
),
);
} }
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));

View file

@ -14,6 +14,8 @@ 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 {
static const _OLD_SP_PATH = "m/352'/1'/0'/#'/0";
BitcoinWalletAddressesBase( BitcoinWalletAddressesBase(
WalletInfo walletInfo, { WalletInfo walletInfo, {
required super.hdWallets, required super.hdWallets,
@ -22,7 +24,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
super.initialAddressesRecords, super.initialAddressesRecords,
super.initialActiveAddressIndex, super.initialActiveAddressIndex,
super.initialAddressPageType, super.initialAddressPageType,
this.loadedFromNewSnapshot = false,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses, List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
List<BitcoinReceivedSPAddressRecord>? initialReceivedSPAddresses, List<BitcoinReceivedSPAddressRecord>? initialReceivedSPAddresses,
}) : silentPaymentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of( }) : silentPaymentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
@ -36,10 +37,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
silentPaymentWallets = [silentPaymentWallet!]; silentPaymentWallets = [silentPaymentWallet!];
} }
final bool loadedFromNewSnapshot;
static const _OLD_SP_PATH = "m/352'/1'/0'/#'/0";
// NOTE: ordered in priority: eg. p2wpkh always first, most used address, etc. // NOTE: ordered in priority: eg. p2wpkh always first, most used address, etc.
@override @override
final walletAddressTypes = [ final walletAddressTypes = [
@ -50,6 +47,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
SegwitAddressType.p2wsh, SegwitAddressType.p2wsh,
]; ];
List<BaseBitcoinAddressRecord> get otherAddresses => silentPaymentAddresses;
@observable @observable
SilentPaymentOwner? silentPaymentWallet; SilentPaymentOwner? silentPaymentWallet;
final ObservableList<BitcoinSilentPaymentAddressRecord> silentPaymentAddresses; final ObservableList<BitcoinSilentPaymentAddressRecord> silentPaymentAddresses;
@ -77,7 +76,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
Future<void> generateInitialSPAddresses() async { Future<void> generateInitialSPAddresses() async {
final addAddresses = silentPaymentAddresses.isEmpty; final addAddresses = silentPaymentAddresses.isEmpty;
// Only initiate these old addresses if restoring a wallet and possibly wants the older cake derivation path // NOTE: Only initiate these old addresses if restoring a wallet and possibly wants the older cake derivation path
if (walletInfo.isRecovery || silentPaymentAddresses.length > 2) { if (walletInfo.isRecovery || silentPaymentAddresses.length > 2) {
final oldScanPath = Bip32PathParser.parse(_OLD_SP_PATH.replaceFirst("#", "1")); final oldScanPath = Bip32PathParser.parse(_OLD_SP_PATH.replaceFirst("#", "1"));
final oldSpendPath = Bip32PathParser.parse(_OLD_SP_PATH.replaceFirst("#", "0")); final oldSpendPath = Bip32PathParser.parse(_OLD_SP_PATH.replaceFirst("#", "0"));
@ -255,43 +254,27 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
@override @override
@action @action
Future<void> updateAddressesInBox() async { Future<void> updateAddressesInBox() async {
// receiveAddressesMapped.entries.forEach((e) { await super.updateAddressesInBox();
// final addressType = e.key;
// final addresses = e.value;
// for (final addr in addresses) { final addr = activeSilentAddress ??
// if (getIsReceive(addr)) { silentPaymentAddresses.firstWhereOrNull(
// allAddressesMap[addr.address] = addr.name; (addressRecord) => !addressRecord.isHidden && !addressRecord.isChange,
);
// final isCurrentType = addr.type == addressPageType; if (addr != null) {
final addressString =
'${addr.address.substring(0, 9 + 5)}...${addr.address.substring(addr.address.length - 9, addr.address.length)}';
// if (addressType == SilentPaymentsAddresType.p2sp) { final isCurrentType = addressPageType == SilentPaymentsAddresType.p2sp;
// final addressString =
// '${addr.address.substring(0, 9 + 5)}...${addr.address.substring(addr.address.length - 9, addr.address.length)}';
// if (!isCurrentType) { if (!isCurrentType) {
// addressesMap[addr.address] = addr.name.isEmpty addressesMap[addr.address] = addr.name.isEmpty
// ? "Silent Payments" + ': $addressString' ? "Silent Payments" + ': $addressString'
// : "Silent Payments - " + addr.name + ': $addressString'; : "Silent Payments - " + addr.name + ': $addressString';
// } else { } else {
// addressesMap[address] = 'Active - Silent Payments' + ': $addressString'; addressesMap[addr.address] = 'Active - Silent Payments' + ': $addressString';
// } }
}
// // Silent Payments address don't break the loop because all are used
// // break;
// } else {
// if (!isCurrentType) {
// addressesMap[addr.address] = '${addressType.value.toUpperCase()}: ${addr.address}';
// } else {
// addressesMap[address] = 'Active - ${addressType.value.toUpperCase()}: $address';
// }
// // Break the loop, already got the firt unused address
// break;
// }
// }
// }
// });
await saveAddressesInBox(); await saveAddressesInBox();
} }
@ -332,6 +315,16 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
addressesSet.addAll(addresses); addressesSet.addAll(addresses);
this.receivedSPAddresses.clear(); this.receivedSPAddresses.clear();
this.receivedSPAddresses.addAll(addressesSet); this.receivedSPAddresses.addAll(addressesSet);
for (final receivedSPAddress in receivedSPAddresses) {
for (final addressRecord in silentPaymentAddresses) {
if (receivedSPAddress.spAddress == addressRecord.address) {
addressRecord.balance.confirmed += receivedSPAddress.balance.confirmed;
addressRecord.txCount += 1;
break;
}
}
}
} }
@action @action
@ -403,7 +396,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
silentPaymentAddresses.map((address) => address.toJSON()).toList(); silentPaymentAddresses.map((address) => address.toJSON()).toList();
json['receivedSPAddresses'] = receivedSPAddresses.map((address) => address.toJSON()).toList(); json['receivedSPAddresses'] = receivedSPAddresses.map((address) => address.toJSON()).toList();
json['silentAddressIndex'] = silentAddressIndex.toString(); json['silentAddressIndex'] = silentAddressIndex.toString();
json['loadedFromNewSnapshot'] = true;
return json; return json;
} }
@ -475,7 +467,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
initialActiveAddressIndex: electrumJson.activeIndexByType, initialActiveAddressIndex: electrumJson.activeIndexByType,
initialSilentAddresses: initialSilentAddresses, initialSilentAddresses: initialSilentAddresses,
initialReceivedSPAddresses: initialReceivedSPAddresses, initialReceivedSPAddresses: initialReceivedSPAddresses,
loadedFromNewSnapshot: snp['loadedFromNewSnapshot'] as bool? ?? false,
); );
} }
} }

View file

@ -158,7 +158,7 @@ class BitcoinWalletService extends WalletService<
xpub: credentials.hwAccountData.xpub, xpub: credentials.hwAccountData.xpub,
walletInfo: credentials.walletInfo!, walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource, unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network, network: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect), encryptionFileUtils: encryptionFileUtilsFor(isDirect),
hdWallets: hdWallets, hdWallets: hdWallets,
); );

View file

@ -16,6 +16,20 @@ class ElectrumBalance extends Balance {
secondAdditional: secondUnconfirmed, secondAdditional: secondUnconfirmed,
); );
factory ElectrumBalance.zero() {
return ElectrumBalance(
confirmed: 0,
unconfirmed: 0,
frozen: 0,
secondConfirmed: 0,
secondUnconfirmed: 0,
);
}
bool hasBalance() {
return confirmed > 0 || unconfirmed > 0 || frozen > 0;
}
static ElectrumBalance? fromJSON(String? jsonSource) { static ElectrumBalance? fromJSON(String? jsonSource) {
if (jsonSource == null) { if (jsonSource == null) {
return null; return null;
@ -61,8 +75,8 @@ class ElectrumBalance extends Balance {
BitcoinAmountUtils.bitcoinAmountToString(amount: secondUnconfirmed); BitcoinAmountUtils.bitcoinAmountToString(amount: secondUnconfirmed);
@override @override
String get formattedFullAvailableBalance => String get formattedFullAvailableBalance => BitcoinAmountUtils.bitcoinAmountToString(
BitcoinAmountUtils.bitcoinAmountToString(amount: (confirmed + unconfirmed) + secondConfirmed - frozen); amount: (confirmed + unconfirmed) + secondConfirmed - frozen);
String toJSON() => json.encode({ String toJSON() => json.encode({
'confirmed': confirmed, 'confirmed': confirmed,

View file

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart'; import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.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';
@ -182,6 +183,56 @@ class ElectrumTransactionInfo extends TransactionInfo {
); );
} }
void updateInputsAndOutputs(ElectrumTransactionBundle bundle, BasedUtxoNetwork network) {
inputAddresses = inputAddresses?.where((address) => address.isNotEmpty).toList();
if (inputAddresses == null ||
inputAddresses!.isEmpty ||
outputAddresses == null ||
outputAddresses!.isEmpty) {
final tempInputAddresses = <String>[];
final tempOutputAddresses = <String>[];
if (bundle.ins.length == bundle.originalTransaction.inputs.length) {
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address =
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network);
if (address.isNotEmpty) tempInputAddresses.add(address);
}
}
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
final out = bundle.originalTransaction.outputs[i];
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) tempOutputAddresses.add(address);
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN')) {
final index = script.indexOf('OP_RETURN');
if (index + 1 <= script.length) {
try {
final opReturnData = script[index + 1].toString();
final decodedString = StringUtils.decode(BytesUtils.fromHexString(opReturnData));
tempOutputAddresses.add('OP_RETURN:$decodedString');
} catch (_) {
tempOutputAddresses.add('OP_RETURN:');
}
}
}
}
inputAddresses = tempInputAddresses;
outputAddresses = tempOutputAddresses;
}
}
factory ElectrumTransactionInfo.fromElectrumBundle( factory ElectrumTransactionInfo.fromElectrumBundle(
ElectrumTransactionBundle bundle, ElectrumTransactionBundle bundle,
WalletType type, WalletType type,
@ -254,9 +305,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
final weSent = ourSentAmounts.length > 0; final weSent = ourSentAmounts.length > 0;
final weReceived = ourReceivedAmounts.length > 0; final weReceived = ourReceivedAmounts.length > 0;
final weReceivedAll = ourReceivedAmounts.length == bundle.originalTransaction.outputs.length;
if (ourReceivedAmounts.length == bundle.originalTransaction.outputs.length) { if (weReceivedAll) {
// All outputs in this tx were received
direction = TransactionDirection.incoming; direction = TransactionDirection.incoming;
amount = ourTotalReceivedAmount; amount = ourTotalReceivedAmount;
} else if (weSent && weReceived && ourTotalSentAmount > ourTotalReceivedAmount) { } else if (weSent && weReceived && ourTotalSentAmount > ourTotalReceivedAmount) {
@ -277,7 +328,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
} }
final fee = totalInputsAmount - totalOutsAmount; final fee = totalInputsAmount - totalOutsAmount;
amount = amount - fee;
return ElectrumTransactionInfo( return ElectrumTransactionInfo(
type, type,

View file

@ -271,7 +271,7 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
BasedUtxoNetwork network; BasedUtxoNetwork network;
// TODO: networks enum // TODO: change from isTestnet boolean to a networks enum (regtest, signet, etc)
@override @override
bool isTestnet; bool isTestnet;
@ -306,11 +306,11 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
void Function(FlutterErrorDetails)? _onError; void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer; Timer? _autoSaveTimer;
Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1; static const int _autoSaveInterval = 1;
Future<bool?> initAddresses([bool? sync]) async { Future<InitAddressesData> initAddresses([bool? sync]) async {
bool? isDiscovered = null; bool? isDiscovered = null;
bool? discovered;
// NOTE: will initiate by priority from the first walletAddressTypes // NOTE: will initiate by priority from the first walletAddressTypes
// then proceeds to following ones after got fully discovered response from worker response // then proceeds to following ones after got fully discovered response from worker response
@ -334,10 +334,22 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
if (isDiscovered == false) { if (isDiscovered == false) {
break; break;
} }
// else if (sync == true)
// subscribeForStatuses(
// walletAddresses.addressesRecords
// .getRecords(
// addressType: addressType,
// seedBytesType: seedBytesType,
// derivationPath: derivationPath,
// isChange: isChange,
// )
// .whereType<BitcoinAddressRecord>()
// .toList(),
// );
} }
if (isDiscovered == false) { if (isDiscovered == false) {
await generateInitialAddresses( discovered = await generateInitialAddresses(
addressType: addressType, addressType: addressType,
seedBytesType: walletAddresses.walletSeedBytesType, seedBytesType: walletAddresses.walletSeedBytesType,
bitcoinDerivationInfo: bitcoinDerivationInfo, bitcoinDerivationInfo: bitcoinDerivationInfo,
@ -346,10 +358,20 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
} }
return isDiscovered; // if (isDiscovered == true && sync == false)
// initAddresses(true);
// else if (isDiscovered == false && discovered == false) initAddresses(sync);
if (isDiscovered == true) syncStatus = SyncedSyncStatus();
return InitAddressesData(
isDiscovered: isDiscovered,
discovered: discovered,
);
} }
Future<void> init() async { Future<void> init() async {
await walletAddresses.init();
await transactionHistory.init(); await transactionHistory.init();
_autoSaveTimer = _autoSaveTimer =
@ -363,13 +385,9 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
syncStatus = SynchronizingSyncStatus(); syncStatus = SynchronizingSyncStatus();
await subscribeForHeaders(true); await subscribeForHeaders(true);
await initAddresses(false);
await updateFeeRates(); await updateFeeRates();
_updateFeeRateTimer ??= await initAddresses(false);
Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
await save(); await save();
} catch (e, stacktrace) { } catch (e, stacktrace) {
@ -437,7 +455,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
} }
// TODO: dynamic
int get dustAmount => 546; int get dustAmount => 546;
bool isBelowDust(int amount) => amount <= dustAmount; bool isBelowDust(int amount) => amount <= dustAmount;
@ -930,7 +947,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
} }
// TODO: LedgerElectrumWallet
void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError();
Future<BtcTransaction> buildHardwareWalletTransaction({ Future<BtcTransaction> buildHardwareWalletTransaction({
@ -1031,6 +1047,7 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
address = fakePublicKey.toP2wshAddress(); address = fakePublicKey.toP2wshAddress();
break; break;
case SegwitAddressType.p2tr: case SegwitAddressType.p2tr:
case SilentPaymentsAddresType.p2sp:
address = fakePublicKey.toTaprootAddress(); address = fakePublicKey.toTaprootAddress();
break; break;
case SegwitAddressType.mweb: case SegwitAddressType.mweb:
@ -1115,7 +1132,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
_receivePort?.close(); _receivePort?.close();
} catch (_) {} } catch (_) {}
_autoSaveTimer?.cancel(); _autoSaveTimer?.cancel();
_updateFeeRateTimer?.cancel();
} }
@action @action
@ -1125,6 +1141,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
]) async { ]) async {
scripthashes ??= walletAddresses.allScriptHashes; scripthashes ??= walletAddresses.allScriptHashes;
if (scripthashes.isEmpty) {
return null;
}
if (wait == true) { if (wait == true) {
return ElectrumWorkerListUnspentResponse.fromJson( return ElectrumWorkerListUnspentResponse.fromJson(
await waitSendWorker( await waitSendWorker(
@ -1163,8 +1183,8 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
final updatedUnspentCoins = <BitcoinUnspent>[]; final updatedUnspentCoins = <BitcoinUnspent>[];
await Future.wait(unspents.entries.map((entry) async { await Future.wait(unspents.entries.map((entry) async {
final unspent = entry.value;
final scriptHash = entry.key; final scriptHash = entry.key;
final unspent = entry.value;
final addressRecord = walletAddresses.allAddresses.firstWhereOrNull( final addressRecord = walletAddresses.allAddresses.firstWhereOrNull(
(element) => element.scriptHash == scriptHash, (element) => element.scriptHash == scriptHash,
@ -1174,12 +1194,19 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
return null; return null;
} }
if (unspent.isEmpty) {
unspentCoins.removeWhere((coin) =>
coin.bitcoinAddressRecord is BitcoinAddressRecord &&
(coin.bitcoinAddressRecord as BitcoinAddressRecord).scriptHash == scriptHash);
return null;
}
await Future.wait(unspent.map((unspent) async { await Future.wait(unspent.map((unspent) async {
final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson()); final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson());
coin.isChange = addressRecord.isChange; coin.isChange = addressRecord.isChange;
// <-
// TODO: \/ final tx = await getExistingTransaction(hash: coin.hash);
final tx = await fetchTransactionInfo(hash: coin.hash);
if (tx != null) { if (tx != null) {
coin.confirmations = tx.confirmations; coin.confirmations = tx.confirmations;
} }
@ -1194,7 +1221,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
await refreshUnspentCoinsInfo(); await refreshUnspentCoinsInfo();
} }
// TODO: move to ElectrumUnspents
@action @action
Future<void> addCoinInfo(BitcoinUnspent coin) async { Future<void> addCoinInfo(BitcoinUnspent coin) async {
// Check if the coin is already in the unspentCoinsInfo for the wallet // Check if the coin is already in the unspentCoinsInfo for the wallet
@ -1328,9 +1354,11 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async { Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
if (tx.confirmations > 0) return null;
try { try {
final bundle = await getTransactionExpanded(hash: tx.txHash); final bundle = await getTransactionExpanded(hash: tx.txHash);
_updateInputsAndOutputs(tx, bundle); tx.updateInputsAndOutputs(bundle, network);
if (bundle.confirmations > 0) return null; if (bundle.confirmations > 0) return null;
return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null; return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null;
} catch (e) { } catch (e) {
@ -1631,6 +1659,12 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
).result; ).result;
} }
Future<ElectrumTransactionInfo?> getExistingTransaction({required String hash}) async {
var tx = transactionHistory.transactions.values.firstWhereOrNull((tx) => tx.txHash == hash);
tx ??= await fetchTransactionInfo(hash: hash);
return tx;
}
Future<ElectrumTransactionInfo?> fetchTransactionInfo({required String hash, int? height}) async { Future<ElectrumTransactionInfo?> fetchTransactionInfo({required String hash, int? height}) async {
try { try {
return ElectrumTransactionInfo.fromElectrumBundle( return ElectrumTransactionInfo.fromElectrumBundle(
@ -1694,6 +1728,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
addressByScripthashes[addressRecord.scriptHash] = addressRecord.address; addressByScripthashes[addressRecord.scriptHash] = addressRecord.address;
}); });
if (scripthashByAddress.isEmpty || addressByScripthashes.isEmpty) {
return null;
}
if (wait == true) if (wait == true)
return ElectrumWorkerScripthashesSubscribeResponse.fromJson( return ElectrumWorkerScripthashesSubscribeResponse.fromJson(
await waitSendWorker( await waitSendWorker(
@ -1715,10 +1753,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
@action @action
void onBalanceResponse(ElectrumBalance balanceResult) { void onBalanceResponse(ElectrumGetBalanceResponse balanceResults) {
var totalFrozen = 0; var totalFrozen = 0;
var totalConfirmed = balanceResult.confirmed; var totalConfirmed = 0;
var totalUnconfirmed = balanceResult.unconfirmed; var totalUnconfirmed = 0;
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) { unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
if (unspentCoinInfo.isFrozen) { if (unspentCoinInfo.isFrozen) {
@ -1726,6 +1764,30 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
}); });
for (int i = 0; i < balanceResults.balances.length; i++) {
final scripthash = balanceResults.scripthashes[i];
final addressRecord = walletAddresses.allAddresses.firstWhereOrNull(
(element) => element.scriptHash == scripthash,
);
if (addressRecord != null) {
final balance = balanceResults.balances[i];
addressRecord.balance = balance;
}
}
for (final addressRecord in [
...walletAddresses.allAddresses,
...walletAddresses.otherAddresses
]) {
final balance = addressRecord.balance;
if (balance.hasBalance()) {
totalConfirmed += balance.confirmed;
totalUnconfirmed += balance.unconfirmed;
}
}
balance[currency] = ElectrumBalance( balance[currency] = ElectrumBalance(
confirmed: totalConfirmed, confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed, unconfirmed: totalUnconfirmed,
@ -1740,6 +1802,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
]) async { ]) async {
scripthashes ??= walletAddresses.allScriptHashes; scripthashes ??= walletAddresses.allScriptHashes;
if (scripthashes.isEmpty) {
return null;
}
if (wait == true) { if (wait == true) {
return ElectrumWorkerGetBalanceResponse.fromJson( return ElectrumWorkerGetBalanceResponse.fromJson(
await waitSendWorker( await waitSendWorker(
@ -1897,55 +1963,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
} }
} }
void _updateInputsAndOutputs(ElectrumTransactionInfo tx, ElectrumTransactionBundle bundle) {
tx.inputAddresses = tx.inputAddresses?.where((address) => address.isNotEmpty).toList();
if (tx.inputAddresses == null ||
tx.inputAddresses!.isEmpty ||
tx.outputAddresses == null ||
tx.outputAddresses!.isEmpty) {
List<String> inputAddresses = [];
List<String> outputAddresses = [];
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
final vout = input.txIndex;
final outTransaction = inputTransaction.outputs[vout];
final address =
BitcoinAddressUtils.addressFromOutputScript(outTransaction.scriptPubKey, network);
if (address.isNotEmpty) inputAddresses.add(address);
}
for (int i = 0; i < bundle.originalTransaction.outputs.length; i++) {
final out = bundle.originalTransaction.outputs[i];
final address = BitcoinAddressUtils.addressFromOutputScript(out.scriptPubKey, network);
if (address.isNotEmpty) outputAddresses.add(address);
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN')) {
final index = script.indexOf('OP_RETURN');
if (index + 1 <= script.length) {
try {
final opReturnData = script[index + 1].toString();
final decodedString = StringUtils.decode(BytesUtils.fromHexString(opReturnData));
outputAddresses.add('OP_RETURN:$decodedString');
} catch (_) {
outputAddresses.add('OP_RETURN:');
}
}
}
}
tx.inputAddresses = inputAddresses;
tx.outputAddresses = outputAddresses;
transactionHistory.addOne(tx);
}
}
@action @action
Future<void> _onAddressesDiscovered(List<BitcoinAddressRecord> addresses) async { Future<void> _onAddressesDiscovered(List<BitcoinAddressRecord> addresses) async {
final scripthashByAddress = await subscribeForStatuses(addresses, true); final scripthashByAddress = await subscribeForStatuses(addresses, true);
@ -2017,70 +2034,40 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
isElectrum: seedBytesType.isElectrum, isElectrum: seedBytesType.isElectrum,
); );
final existingReceiveAddresses = walletAddresses.addressesRecords.getRecords( for (final isChange in [true, false]) {
final existingAddresses = walletAddresses.addressesRecords.getRecords(
addressType: addressType, addressType: addressType,
seedBytesType: seedBytesType, seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(), derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: false, isChange: isChange,
); );
final discoveredExistingReceiveAddresses = final discoveredExistingAddresses = walletAddresses.discoveredAddressesRecord.getIsDiscovered(
walletAddresses.discoveredAddressesRecord.getIsDiscovered(
addressType: addressType, addressType: addressType,
seedBytesType: seedBytesType, seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(), derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: false, isChange: isChange,
); );
if (!discoveredExistingReceiveAddresses && final gapCount = isChange
existingReceiveAddresses.length < ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT) { ? ElectrumWalletAddressesBase.INITIAL_CHANGE_COUNT
: ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT;
if (!discoveredExistingAddresses && existingAddresses.length < gapCount) {
discoverNewAddresses( discoverNewAddresses(
addressType: addressType, addressType: addressType,
seedBytesType: seedBytesType, seedBytesType: seedBytesType,
derivationInfo: bitcoinDerivationInfo, derivationInfo: bitcoinDerivationInfo,
isChange: false, isChange: isChange,
startIndex: existingReceiveAddresses.length, startIndex: existingAddresses.length,
); );
discovered = true; discovered = true;
} else {
walletAddresses.discoveredAddressesRecord.addDiscovered(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: false,
discovered: true,
);
} }
final existingChangeAddresses = walletAddresses.addressesRecords.getRecords(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: true,
);
final discoveredExistingChangeAddresses =
walletAddresses.discoveredAddressesRecord.getIsDiscovered(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: true,
);
if (!discoveredExistingChangeAddresses &&
existingChangeAddresses.length < ElectrumWalletAddressesBase.INITIAL_CHANGE_COUNT) {
discoverNewAddresses(
addressType: addressType,
seedBytesType: seedBytesType,
derivationInfo: bitcoinDerivationInfo,
isChange: true,
startIndex: existingChangeAddresses.length,
);
discovered = true;
} else {
walletAddresses.discoveredAddressesRecord.addDiscovered( walletAddresses.discoveredAddressesRecord.addDiscovered(
addressType: addressType, addressType: addressType,
seedBytesType: seedBytesType, seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(), derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: true, isChange: isChange,
discovered: true, discovered: true,
); );
} }
@ -2119,8 +2106,7 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
needsToDiscover = recordList.sublist(recordList.length - countToDiscover).any( needsToDiscover = recordList.sublist(recordList.length - countToDiscover).any(
(record) { (record) {
return scripthashStatuses?.any( return scripthashStatuses?.any(
(scripthashStatus) => (scripthashStatus) => scripthashStatus.scripthash == record.scriptHash,
scripthashStatus.scripthash == (record as BitcoinAddressRecord).scriptHash,
) ?? ) ??
record.getIsUsed(); record.getIsUsed();
}, },
@ -2146,6 +2132,12 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
).toJson(), ).toJson(),
); );
} }
@override
String formatCryptoAmount(String amount) {
final amountInt = int.parse(amount);
return BitcoinAmountUtils.bitcoinAmountToString(amount: amountInt);
}
} }
class ElectrumEstimatedTx { class ElectrumEstimatedTx {
@ -2255,3 +2247,13 @@ class CreateTxData {
required this.memo, required this.memo,
}); });
} }
class InitAddressesData {
bool? isDiscovered;
bool? discovered;
InitAddressesData({
required this.isDiscovered,
required this.discovered,
});
}

View file

@ -49,9 +49,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
String get xpub => hdWallet.publicKey.toExtended; String get xpub => hdWallet.publicKey.toExtended;
String get xpriv => hdWallet.privateKey.toExtended; String get xpriv => hdWallet.privateKey.toExtended;
// NOTE: order matters in priority
List<SeedBytesType> get seedBytesTypes { List<SeedBytesType> get seedBytesTypes {
final seedBytesTypes = <SeedBytesType>[]; final seedBytesTypes = <SeedBytesType>[];
// NOTE: order matters in priority
if (hdWallets.containsKey(SeedBytesType.bip39)) { if (hdWallets.containsKey(SeedBytesType.bip39)) {
seedBytesTypes.add(SeedBytesType.bip39); seedBytesTypes.add(SeedBytesType.bip39);
} }
@ -84,7 +84,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
@computed @computed
BitcoinDerivationInfo get _defaultAddressPageDerivationInfo => BitcoinDerivationInfo get defaultAddressPageDerivationInfo =>
BitcoinAddressUtils.getDerivationFromType( BitcoinAddressUtils.getDerivationFromType(
addressPageType, addressPageType,
network: network, network: network,
@ -100,8 +100,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@observable @observable
BitcoinAddressRecordMap addressesRecords; BitcoinAddressRecordMap addressesRecords;
Set<String> get hiddenAddresses => @observable
allAddresses.where((address) => address.isHidden).map((address) => address.address).toSet(); Set<String> _hiddenAddresses = {};
@computed
Set<String> get hiddenAddresses => _hiddenAddresses;
List<BaseBitcoinAddressRecord> get otherAddresses => [];
@observable @observable
List<BitcoinAddressRecord> _allAddresses = []; List<BitcoinAddressRecord> _allAddresses = [];
@ -110,32 +114,53 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
List<BitcoinAddressRecord> updateAllAddresses() { List<BitcoinAddressRecord> updateAllAddresses() {
_allAddresses = addressesRecords.allRecords().whereType<BitcoinAddressRecord>().toList(); _allAddresses = addressesRecords.allRecords().toList();
updateAllScriptHashes();
updateSelectedReceiveAddresses(); updateSelectedReceiveAddresses();
updateSelectedChangeAddresses(); updateSelectedChangeAddresses();
allAddressesMap.clear(); allAddressesMap.clear();
_allAddresses.forEach((addressRecord) {
final hiddenAddresses = <String>{};
final allScriptHashes = <String>[];
for (final addressRecord in allAddresses) {
allAddressesMap[addressRecord.address] = addressRecord.name; allAddressesMap[addressRecord.address] = addressRecord.name;
}); allScriptHashes.add(addressRecord.scriptHash);
if (addressRecord.isHidden) {
hiddenAddresses.add(addressRecord.address);
}
}
// NOTE: "otherAddresses" is used for wallets with different address types
// i.e. btc with silent paymnets or ltc with mweb
for (final addressRecord in otherAddresses) {
allAddressesMap[addressRecord.address] = addressRecord.name;
if (addressRecord.isHidden) {
hiddenAddresses.add(addressRecord.address);
}
}
_allScriptHashes = allScriptHashes;
_hiddenAddresses = hiddenAddresses;
return _allAddresses; return _allAddresses;
} }
@observable @observable
// NOTE: Selected Receive Addresses = Receive addresses selected by current receive page type // NOTE: Selected Receive Addresses = Receive addresses selected by current receive page type
List<BaseBitcoinAddressRecord> _selectedReceiveAddresses = []; List<BitcoinAddressRecord> _selectedReceiveAddresses = [];
@computed @computed
List<BaseBitcoinAddressRecord> get selectedReceiveAddresses => _selectedReceiveAddresses; List<BitcoinAddressRecord> get selectedReceiveAddresses => _selectedReceiveAddresses;
@action @action
List<BaseBitcoinAddressRecord> updateSelectedReceiveAddresses() { List<BitcoinAddressRecord> updateSelectedReceiveAddresses() {
_selectedReceiveAddresses = addressesRecords.getRecords( _selectedReceiveAddresses = addressesRecords.getRecords(
addressType: addressPageType, addressType: addressPageType,
seedBytesType: walletSeedBytesType, seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(), derivationPath: defaultAddressPageDerivationInfo.derivationPath.toString(),
isChange: false, isChange: false,
); );
updateNextReceiveAddress(); updateNextReceiveAddress();
@ -144,17 +169,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@observable @observable
// NOTE: Usable Receive Addresses = Receive addresses usable based on auto generate setting // NOTE: Usable Receive Addresses = Receive addresses usable based on auto generate setting
List<BaseBitcoinAddressRecord> _usableReceiveAddresses = []; List<BitcoinAddressRecord> _usableReceiveAddresses = [];
@computed @computed
List<BaseBitcoinAddressRecord> get usableReceiveAddresses => _usableReceiveAddresses; List<BitcoinAddressRecord> get usableReceiveAddresses => _usableReceiveAddresses;
@action @action
List<BaseBitcoinAddressRecord> updateUsableReceiveAddresses() { List<BitcoinAddressRecord> updateUsableReceiveAddresses() {
_usableReceiveAddresses = addressesRecords _usableReceiveAddresses = addressesRecords
.getRecords( .getRecords(
addressType: addressPageType, addressType: addressPageType,
seedBytesType: walletSeedBytesType, seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(), derivationPath: defaultAddressPageDerivationInfo.derivationPath.toString(),
isChange: false, isChange: false,
) )
.where( .where(
@ -171,32 +196,27 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
BitcoinAddressRecord? updateNextReceiveAddress() { BitcoinAddressRecord? updateNextReceiveAddress() {
final receiveAddresses = selectedReceiveAddresses.whereType<BitcoinAddressRecord>(); final receiveAddresses = selectedReceiveAddresses;
if (receiveAddresses.isEmpty) { if (receiveAddresses.isEmpty) {
return null; return null;
} }
_nextReceiveAddress = receiveAddresses.firstWhereOrNull( _nextReceiveAddress = receiveAddresses.first;
(addressRecord) =>
addressRecord.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) &&
!addressRecord.isChange,
) ??
receiveAddresses.first;
return _nextReceiveAddress; return _nextReceiveAddress;
} }
@observable @observable
// NOTE: Selected Change Addresses = Change addresses selected by current receive page type // NOTE: Selected Change Addresses = Change addresses selected by current receive page type
List<BaseBitcoinAddressRecord> _selectedChangeAddresses = []; List<BitcoinAddressRecord> _selectedChangeAddresses = [];
@computed @computed
List<BaseBitcoinAddressRecord> get selectedChangeAddresses => _selectedChangeAddresses; List<BitcoinAddressRecord> get selectedChangeAddresses => _selectedChangeAddresses;
@action @action
List<BaseBitcoinAddressRecord> updateSelectedChangeAddresses() { List<BitcoinAddressRecord> updateSelectedChangeAddresses() {
_selectedChangeAddresses = addressesRecords.getRecords( _selectedChangeAddresses = addressesRecords.getRecords(
addressType: addressPageType, addressType: addressPageType,
seedBytesType: walletSeedBytesType, seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(), derivationPath: defaultAddressPageDerivationInfo.derivationPath.toString(),
isChange: true, isChange: true,
); );
updateNextChangeAddress(); updateNextChangeAddress();
@ -206,17 +226,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@observable @observable
// NOTE: Usable Receive Addresses = Receive addresses usable based on auto generate setting // NOTE: Usable Receive Addresses = Receive addresses usable based on auto generate setting
List<BaseBitcoinAddressRecord> _usableChangeAddresses = []; List<BitcoinAddressRecord> _usableChangeAddresses = [];
@computed @computed
List<BaseBitcoinAddressRecord> get usableChangeAddresses => _usableChangeAddresses; List<BitcoinAddressRecord> get usableChangeAddresses => _usableChangeAddresses;
@action @action
List<BaseBitcoinAddressRecord> updateUsableChangeAddresses() { List<BitcoinAddressRecord> updateUsableChangeAddresses() {
_usableChangeAddresses = addressesRecords _usableChangeAddresses = addressesRecords
.getRecords( .getRecords(
addressType: changeAddressType, addressType: changeAddressType,
seedBytesType: walletSeedBytesType, seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(), derivationPath: BitcoinAddressUtils.getDerivationFromType(
changeAddressType,
network: network,
isElectrum: walletSeedBytesType.isElectrum,
).derivationPath.toString(),
isChange: true, isChange: true,
) )
.where( .where(
@ -233,17 +257,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
BitcoinAddressRecord? updateNextChangeAddress() { BitcoinAddressRecord? updateNextChangeAddress() {
final changeAddresses = selectedChangeAddresses.whereType<BitcoinAddressRecord>(); final changeAddresses = selectedChangeAddresses;
if (changeAddresses.isEmpty) { if (changeAddresses.isEmpty) {
return null; return null;
} }
_nextChangeAddress = changeAddresses.firstWhereOrNull( _nextChangeAddress = changeAddresses.first;
(addressRecord) =>
addressRecord.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) &&
addressRecord.isChange,
) ??
changeAddresses.first;
return _nextChangeAddress; return _nextChangeAddress;
} }
@ -252,18 +271,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed @computed
List<String> get allScriptHashes => _allScriptHashes; List<String> get allScriptHashes => _allScriptHashes;
@action BitcoinAddressRecord getFromAddresses(String address) =>
List<String> updateAllScriptHashes() {
_allScriptHashes.clear();
_allScriptHashes.addAll(allAddresses.map((address) => address.scriptHash));
return _allScriptHashes;
}
BaseBitcoinAddressRecord getFromAddresses(String address) =>
allAddresses.firstWhere((element) => element.address == address); allAddresses.firstWhere((element) => element.address == address);
// TODO: feature with toggle to switch change address type // TODO: add feature to toggle from settings to switch what change address type is used when sending txs
// figure out how to manage on addres page type switch // or add option that the current change address follows the current addres page type
@observable @observable
BitcoinAddressType changeAddressType = SegwitAddressType.p2wpkh; BitcoinAddressType changeAddressType = SegwitAddressType.p2wpkh;
@ -273,7 +285,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
void resetActiveAddress() { void resetActiveAddress() {
try { try {
final activeReceiveAddresses = selectedReceiveAddresses.whereType<BitcoinAddressRecord>(); final activeReceiveAddresses = selectedReceiveAddresses;
activeBitcoinAddress = activeReceiveAddresses.firstWhereOrNull( activeBitcoinAddress = activeReceiveAddresses.firstWhereOrNull(
(addressRecord) => addressRecord.index == activeIndexByType[addressPageType], (addressRecord) => addressRecord.index == activeIndexByType[addressPageType],
@ -311,15 +323,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override @override
@computed @computed
String get primaryAddress => String get primaryAddress => nextReceiveAddress?.address ?? '';
selectedReceiveAddresses
.firstWhereOrNull(
(addressRecord) =>
addressRecord.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) &&
!addressRecord.isChange,
)
?.address ??
'';
Map<BitcoinAddressType, int> activeIndexByType; Map<BitcoinAddressType, int> activeIndexByType;
@ -342,7 +346,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
isChange: false, isChange: false,
index: newAddressIndex, index: newAddressIndex,
addressType: addressPageType, addressType: addressPageType,
derivationInfo: _defaultAddressPageDerivationInfo, derivationInfo: defaultAddressPageDerivationInfo,
hdWallet: hdWallet, hdWallet: hdWallet,
seedBytesType: walletSeedBytesType, seedBytesType: walletSeedBytesType,
network: network, network: network,
@ -433,6 +437,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
Future<void> updateAddressesInBox() async { Future<void> updateAddressesInBox() async {
addressesMap.clear(); addressesMap.clear();
for (final addressType in walletAddressTypes) {
final index = activeIndexByType[addressType] ?? 0;
final address = addressesRecords
.getRecords(
addressType: addressType,
seedBytesType: walletSeedBytesType,
derivationPath: BitcoinAddressUtils.getDerivationFromType(
addressType,
network: network,
isElectrum: walletSeedBytesType.isElectrum,
).derivationPath.toString(),
isChange: false,
)
.firstWhereOrNull((addressRecord) => addressRecord.index == index);
final isCurrentType = addressType == addressPageType;
if (address != null) {
if (isCurrentType) {
addressesMap[address.address] = 'Active - ' + addressPageType.toString() + ': $address';
} else {
addressesMap[address.address] = '${addressType.value.toUpperCase()}: ${address.address}';
}
}
}
addressesMap[address] = 'Active - ' + addressPageType.toString() + ': $address'; addressesMap[address] = 'Active - ' + addressPageType.toString() + ': $address';
await saveAddressesInBox(); await saveAddressesInBox();
@ -440,7 +472,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action @action
void updateAddress(String address, String label) { void updateAddress(String address, String label) {
BaseBitcoinAddressRecord? foundAddress; BitcoinAddressRecord? foundAddress;
for (final addressRecord in _allAddresses) { for (final addressRecord in _allAddresses) {
if (addressRecord.address == address) { if (addressRecord.address == address) {
@ -449,7 +481,6 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
} }
// TODO: verify this updates and keeps on re-open
if (foundAddress != null) { if (foundAddress != null) {
foundAddress.setNewName(label); foundAddress.setNewName(label);
} }
@ -533,7 +564,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
} }
} }
typedef AddressRecords = List<BaseBitcoinAddressRecord>; typedef AddressRecords = List<BitcoinAddressRecord>;
typedef ItemsByIsChange<T> = Map<bool, T>; typedef ItemsByIsChange<T> = Map<bool, T>;
typedef ItemsByDerivationPath<T> = Map<String, ItemsByIsChange<T>>; typedef ItemsByDerivationPath<T> = Map<String, ItemsByIsChange<T>>;
@ -572,7 +603,7 @@ class BitcoinAddressRecordMap extends ItemsRecordMap<AddressRecordsBySeedType> {
required SeedBytesType seedBytesType, required SeedBytesType seedBytesType,
required String derivationPath, required String derivationPath,
required bool isChange, required bool isChange,
required List<BaseBitcoinAddressRecord> addressRecords, required List<BitcoinAddressRecord> addressRecords,
}) { }) {
_data.putIfAbsent( _data.putIfAbsent(
addressType, addressType,
@ -614,18 +645,18 @@ class BitcoinAddressRecordMap extends ItemsRecordMap<AddressRecordsBySeedType> {
_data[addressType]![seedBytesType]![derivationPath]![isChange] = recordsList; _data[addressType]![seedBytesType]![derivationPath]![isChange] = recordsList;
} }
List<BaseBitcoinAddressRecord> allRecords() { List<BitcoinAddressRecord> allRecords() {
return _data.values return _data.values
.expand((seedTypeMap) => seedTypeMap.values) .expand((seedTypeMap) => seedTypeMap.values)
.expand((derivationMap) => derivationMap.values) .expand((derivationMap) => derivationMap.values)
.expand((changeMap) => changeMap.values) .expand((changeMap) => changeMap.values)
.fold<List<BaseBitcoinAddressRecord>>( .fold<List<BitcoinAddressRecord>>(
[], [],
(acc, records) => acc..addAll(records), (acc, records) => acc..addAll(records),
); );
} }
List<BaseBitcoinAddressRecord> getRecords({ List<BitcoinAddressRecord> getRecords({
required BitcoinAddressType addressType, required BitcoinAddressType addressType,
required SeedBytesType seedBytesType, required SeedBytesType seedBytesType,
required String derivationPath, required String derivationPath,
@ -672,7 +703,7 @@ class BitcoinAddressRecordMap extends ItemsRecordMap<AddressRecordsBySeedType> {
(isChange, v) => MapEntry( (isChange, v) => MapEntry(
isChange == 'true', isChange == 'true',
(v as List<dynamic>) (v as List<dynamic>)
.map((address) => BaseBitcoinAddressRecord.fromJSON(address as String)) .map((address) => BitcoinAddressRecord.fromJSON(address as String))
.toList(), .toList(),
), ),
), ),

View file

@ -145,6 +145,56 @@ class ElectrumWorker {
} }
} }
Future<DateResult> _getTxDate(
String txid,
int currentChainTip, {
int? confirmations,
DateTime? date,
}) async {
int? time;
int? height;
bool? isDateValidated;
final mempoolApi = ApiProvider.fromMempool(
_network!,
baseUrl: "https://mempool.cakewallet.com/api/v1",
);
try {
final txVerbose = await mempoolApi.getTransaction<MempoolTransaction>(txid);
final status = txVerbose.status;
height = status.blockHeight;
if (height != null) {
final blockHash = await mempoolApi.getBlockHeight(height);
final block = await mempoolApi.getBlock(blockHash);
time = int.parse(block['timestamp'].toString());
if (date != null) {
final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000);
isDateValidated = newDate == date;
}
}
} catch (_) {}
if (confirmations == null && height != null) {
final tip = currentChainTip;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
return DateResult(
time: time,
height: height,
isDateValidated: isDateValidated,
confirmations: confirmations,
);
}
Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async { Future<void> _handleConnect(ElectrumWorkerConnectionRequest request) async {
_network = request.network; _network = request.network;
_walletType = request.walletType; _walletType = request.walletType;
@ -219,6 +269,9 @@ class ElectrumWorker {
tx.isPending = tx.confirmations == 0; tx.isPending = tx.confirmations == 0;
anyTxWasUpdated = true; anyTxWasUpdated = true;
} }
} else {
tx.height = newChainTip;
tx.confirmations = 0;
} }
}); });
@ -477,10 +530,11 @@ class ElectrumWorker {
final txInfo = ElectrumTransactionInfo.fromElectrumBundle( final txInfo = ElectrumTransactionInfo.fromElectrumBundle(
txBundle, txBundle,
request.walletType, request.walletType,
request.network, _network!,
addresses: addresses.toSet(), addresses: addresses.toSet(),
height: transactionIdsForHeights[hash], height: transactionIdsForHeights[hash],
); );
txInfo.updateInputsAndOutputs(txBundle, _network!);
request.addresses.forEach( request.addresses.forEach(
(addr) { (addr) {
@ -600,7 +654,7 @@ class ElectrumWorker {
final txInfo = ElectrumTransactionInfo.fromElectrumBundle( final txInfo = ElectrumTransactionInfo.fromElectrumBundle(
txBundle, txBundle,
request.walletType, request.walletType,
request.network, _network!,
addresses: addresses.toSet(), addresses: addresses.toSet(),
height: transactionIdsForHeights[hash], height: transactionIdsForHeights[hash],
); );
@ -758,9 +812,8 @@ class ElectrumWorker {
); );
} else if (request.mempoolAPIEnabled) { } else if (request.mempoolAPIEnabled) {
try { try {
date = await getTxDate( date = await _getTxDate(
hash, hash,
_network!,
request.chainTip, request.chainTip,
confirmations: storedTx?.confirmations, confirmations: storedTx?.confirmations,
date: storedTx?.date, date: storedTx?.date,
@ -788,7 +841,7 @@ class ElectrumWorker {
isDateValidated: date?.isDateValidated, isDateValidated: date?.isDateValidated,
), ),
request.walletType, request.walletType,
request.network, _network!,
addresses: addresses.toSet(), addresses: addresses.toSet(),
height: transactionsByIds[hash]?.height, height: transactionsByIds[hash]?.height,
); );
@ -940,9 +993,8 @@ class ElectrumWorker {
); );
} else if (request.mempoolAPIEnabled) { } else if (request.mempoolAPIEnabled) {
try { try {
date = await getTxDate( date = await _getTxDate(
hash, hash,
_network!,
request.chainTip, request.chainTip,
confirmations: storedTx?.confirmations, confirmations: storedTx?.confirmations,
date: storedTx?.date, date: storedTx?.date,
@ -973,7 +1025,7 @@ class ElectrumWorker {
isDateValidated: date?.isDateValidated, isDateValidated: date?.isDateValidated,
), ),
request.walletType, request.walletType,
request.network, _network!,
addresses: addresses.toSet(), addresses: addresses.toSet(),
height: transactionsByIds[hash]?.height, height: transactionsByIds[hash]?.height,
); );
@ -1023,37 +1075,6 @@ class ElectrumWorker {
})); }));
} }
// Future<void> _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async {
// final balanceFutures = <Future<Map<String, dynamic>>>[];
// for (final scripthash in request.scripthashes) {
// final balanceFuture = _electrumClient!.request(
// ElectrumGetScriptHashBalance(scriptHash: scripthash),
// );
// balanceFutures.add(balanceFuture);
// }
// var totalConfirmed = 0;
// var totalUnconfirmed = 0;
// final balances = await Future.wait(balanceFutures);
// for (final balance in balances) {
// final confirmed = balance['confirmed'] as int? ?? 0;
// final unconfirmed = balance['unconfirmed'] as int? ?? 0;
// totalConfirmed += confirmed;
// totalUnconfirmed += unconfirmed;
// }
// _sendResponse(ElectrumWorkerGetBalanceResponse(
// result: ElectrumBalance(
// confirmed: totalConfirmed,
// unconfirmed: totalUnconfirmed,
// frozen: 0,
// ),
// ));
// }
Future<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { Future<void> _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async {
final scripthashes = request.scripthashes.where((s) => s.isNotEmpty).toList(); final scripthashes = request.scripthashes.where((s) => s.isNotEmpty).toList();
final balanceResults = <Map<String, dynamic>>[]; final balanceResults = <Map<String, dynamic>>[];
@ -1078,22 +1099,26 @@ class ElectrumWorker {
})); }));
} }
var totalConfirmed = 0; final balances = <ElectrumBalance>[];
var totalUnconfirmed = 0;
for (final balance in balanceResults) { for (final balance in balanceResults) {
final confirmed = balance['confirmed'] as int? ?? 0; final confirmed = balance['confirmed'] as int? ?? 0;
final unconfirmed = balance['unconfirmed'] as int? ?? 0; final unconfirmed = balance['unconfirmed'] as int? ?? 0;
totalConfirmed += confirmed;
totalUnconfirmed += unconfirmed; balances.add(
ElectrumBalance(
confirmed: confirmed,
unconfirmed: unconfirmed,
frozen: 0,
),
);
} }
_sendResponse( _sendResponse(
ElectrumWorkerGetBalanceResponse( ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance( result: ElectrumGetBalanceResponse(
confirmed: totalConfirmed, balances: balances,
unconfirmed: totalUnconfirmed, scripthashes: scripthashes,
frozen: 0,
), ),
id: request.id, id: request.id,
), ),
@ -1114,9 +1139,7 @@ class ElectrumWorker {
) )
.timeout(const Duration(seconds: 3)); .timeout(const Duration(seconds: 3));
if (scriptHashUnspents.isNotEmpty) {
unspents[scriptHash] = scriptHashUnspents; unspents[scriptHash] = scriptHashUnspents;
}
}); });
_sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id)); _sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id));
@ -1337,9 +1360,8 @@ class ElectrumWorker {
if (getTime && _walletType == WalletType.bitcoin) { if (getTime && _walletType == WalletType.bitcoin) {
if (mempoolAPIEnabled) { if (mempoolAPIEnabled) {
try { try {
dates = await getTxDate( dates = await _getTxDate(
hash, hash,
_network!,
currentChainTip, currentChainTip,
confirmations: confirmations, confirmations: confirmations,
date: date, date: date,
@ -1418,7 +1440,7 @@ class ElectrumWorker {
int halfHour = recommendedFees.medium.satoshis; int halfHour = recommendedFees.medium.satoshis;
int fastest = recommendedFees.high.satoshis; int fastest = recommendedFees.high.satoshis;
// Bitcoin only: adjust fee rates to avoid equal fee values // Adjust fee rates to avoid equal fee values
// elevated fee should be higher than normal fee // elevated fee should be higher than normal fee
if (hour == halfHour) { if (hour == halfHour) {
halfHour++; halfHour++;
@ -1566,6 +1588,7 @@ class ElectrumWorker {
final syncingStatus = scanData.isSingleScan final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0) ? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight); : SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse( _sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse( result: TweaksSyncResponse(
height: syncHeight, height: syncHeight,
@ -1608,11 +1631,7 @@ class ElectrumWorker {
} }
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s) // placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
final txDate = await getTxDate( final txDate = await _getTxDate(txid, scanData.chainTip);
txid,
scanData.network,
scanData.chainTip,
);
final txInfo = ElectrumTransactionInfo( final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin, WalletType.bitcoin,
@ -1651,6 +1670,8 @@ class ElectrumWorker {
); );
final labelIndex = labelValue != null ? scanData.labels[label] : 0; final labelIndex = labelValue != null ? scanData.labels[label] : 0;
final balance = ElectrumBalance.zero();
balance.confirmed = amount;
final receivedAddressRecord = BitcoinReceivedSPAddressRecord( final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
receivingOutputAddress, receivingOutputAddress,
@ -1661,7 +1682,7 @@ class ElectrumWorker {
isUsed: true, isUsed: true,
tweak: t_k, tweak: t_k,
txCount: 1, txCount: 1,
balance: amount, balance: balance,
spAddress: matchingSPWallet.toAddress(scanData.network), spAddress: matchingSPWallet.toAddress(scanData.network),
); );
@ -1739,10 +1760,10 @@ class ElectrumWorker {
derivationInfo: request.derivationInfo, derivationInfo: request.derivationInfo,
hdWallet: Bip32Slip10Secp256k1.fromExtendedKey( hdWallet: Bip32Slip10Secp256k1.fromExtendedKey(
request.xpriv, request.xpriv,
BitcoinAddressUtils.getKeyNetVersion(request.network), BitcoinAddressUtils.getKeyNetVersion(_network!),
), ),
seedBytesType: request.seedBytesType, seedBytesType: request.seedBytesType,
network: request.network, network: _network!,
); );
newAddresses.add(addressRecord); newAddresses.add(addressRecord);
@ -1785,57 +1806,6 @@ class DateResult {
}); });
} }
Future<DateResult> getTxDate(
String txid,
BasedUtxoNetwork network,
int currentChainTip, {
int? confirmations,
DateTime? date,
}) async {
int? time;
int? height;
bool? isDateValidated;
final mempoolApi = ApiProvider.fromMempool(
network,
baseUrl: "https://mempool.cakewallet.com/api/v1",
);
try {
final txVerbose = await mempoolApi.getTransaction<MempoolTransaction>(txid);
final status = txVerbose.status;
height = status.blockHeight;
if (height != null) {
final blockHash = await mempoolApi.getBlockHeight(height);
final block = await mempoolApi.getBlock(blockHash);
time = int.parse(block['timestamp'].toString());
if (date != null) {
final newDate = DateTime.fromMillisecondsSinceEpoch(time * 1000);
isDateValidated = newDate == date;
}
}
} catch (_) {}
if (confirmations == null && height != null) {
final tip = currentChainTip;
if (tip > 0 && height > 0) {
// Add one because the block itself is the first confirmation
confirmations = tip - height + 1;
}
}
return DateResult(
time: time,
height: height,
isDateValidated: isDateValidated,
confirmations: confirmations,
);
}
class TxToFetch { class TxToFetch {
final ElectrumTransactionInfo? tx; final ElectrumTransactionInfo? tx;
final int height; final int height;

View file

@ -44,8 +44,44 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
final String method = ElectrumRequestMethods.getBalance.method; final String method = ElectrumRequestMethods.getBalance.method;
} }
class ElectrumGetBalanceResponse {
ElectrumGetBalanceResponse({
required this.balances,
required this.scripthashes,
});
final List<ElectrumBalance> balances;
final List<String> scripthashes;
Map<String, dynamic> toJson() {
return {
'balances': balances
.map((e) => {
'confirmed': e.confirmed,
'unconfirmed': e.unconfirmed,
'frozen': e.frozen,
})
.toList(),
'scripthashes': scripthashes,
};
}
factory ElectrumGetBalanceResponse.fromJson(Map<String, dynamic> json) {
return ElectrumGetBalanceResponse(
balances: (json['balances'] as List)
.map((e) => ElectrumBalance(
confirmed: e['confirmed'] as int,
unconfirmed: e['unconfirmed'] as int,
frozen: e['frozen'] as int,
))
.toList(),
scripthashes: (json['scripthashes'] as List).cast<String>(),
);
}
}
class ElectrumWorkerGetBalanceResponse class ElectrumWorkerGetBalanceResponse
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> { extends ElectrumWorkerResponse<ElectrumGetBalanceResponse, Map<String, dynamic>> {
ElectrumWorkerGetBalanceResponse({ ElectrumWorkerGetBalanceResponse({
required super.result, required super.result,
super.error, super.error,
@ -54,18 +90,14 @@ class ElectrumWorkerGetBalanceResponse
}) : super(method: ElectrumRequestMethods.getBalance.method); }) : super(method: ElectrumRequestMethods.getBalance.method);
@override @override
Map<String, int>? resultJson(result) { Map<String, dynamic> resultJson(result) {
return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed}; return result.toJson();
} }
@override @override
factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) { factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceResponse( return ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance( result: ElectrumGetBalanceResponse.fromJson(json['result'] as Map<String, dynamic>),
confirmed: json['result']['confirmed'] as int,
unconfirmed: json['result']['unconfirmed'] as int,
frozen: 0,
),
error: json['error'] as String?, error: json['error'] as String?,
id: json['id'] as int?, id: json['id'] as int?,
completed: json['completed'] as bool? ?? false, completed: json['completed'] as bool? ?? false,

View file

@ -450,12 +450,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
// reset coin balances and txCount to 0: // reset coin balances and txCount to 0:
unspentCoins.forEach((coin) { unspentCoins.forEach((coin) {
coin.bitcoinAddressRecord.balance = 0; coin.bitcoinAddressRecord.balance = ElectrumBalance.zero();
coin.bitcoinAddressRecord.txCount = 0; coin.bitcoinAddressRecord.txCount = 0;
}); });
for (var addressRecord in walletAddresses.mwebAddresses) { for (var addressRecord in walletAddresses.mwebAddresses) {
addressRecord.balance = 0; addressRecord.balance = ElectrumBalance.zero();
addressRecord.txCount = 0; addressRecord.txCount = 0;
} }
@ -520,7 +520,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
// update the txCount: // update the txCount:
addressRecord.txCount++; addressRecord.txCount++;
addressRecord.balance += utxo.value.toInt(); addressRecord.balance.confirmed += utxo.value.toInt();
addressRecord.setAsUsed(); addressRecord.setAsUsed();
} }
@ -676,7 +676,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
if (!inputAddresses.contains(utxo.address)) { if (!inputAddresses.contains(utxo.address)) {
addressRecord.txCount++; addressRecord.txCount++;
} }
addressRecord.balance -= utxo.value.toInt(); addressRecord.balance.confirmed -= utxo.value.toInt();
amount += utxo.value.toInt(); amount += utxo.value.toInt();
inputAddresses.add(utxo.address); inputAddresses.add(utxo.address);
input.add(hex.decode(outputId)); input.add(hex.decode(outputId));
@ -855,7 +855,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
} catch (_) {} } catch (_) {}
for (var addressRecord in walletAddresses.mwebAddresses) { for (var addressRecord in walletAddresses.mwebAddresses) {
addressRecord.balance = 0; addressRecord.balance = ElectrumBalance.zero();
addressRecord.txCount = 0; addressRecord.txCount = 0;
} }
@ -873,7 +873,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
coin.isFrozen = coinInfo.isFrozen; coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending; coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note; coin.note = coinInfo.note;
coin.bitcoinAddressRecord.balance += coinInfo.value; coin.bitcoinAddressRecord.balance.confirmed += coinInfo.value;
} else { } else {
super.addCoinInfo(coin); super.addCoinInfo(coin);
} }
@ -1537,7 +1537,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
if (!addresses.contains(utxo.address)) { if (!addresses.contains(utxo.address)) {
addresses.add(utxo.address); addresses.add(utxo.address);
} }
addressRecord.balance -= utxo.value.toInt(); addressRecord.balance.confirmed -= utxo.value.toInt();
}); });
transaction.inputAddresses?.addAll(addresses); transaction.inputAddresses?.addAll(addresses);
printV("isPegIn: $isPegIn, isPegOut: $isPegOut"); printV("isPegIn: $isPegIn, isPegOut: $isPegOut");

View file

@ -67,24 +67,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
Future<void> init() async { Future<void> init() async {
if (!super.isHardwareWallet) await initMwebAddresses(); if (!super.isHardwareWallet) await initMwebAddresses();
// for (final seedBytesType in hdWallets.keys) { super.init();
// await generateInitialAddresses(
// addressType: SegwitAddressType.p2wpkh,
// seedBytesType: seedBytesType,
// bitcoinDerivationInfo: seedBytesType.isElectrum
// ? BitcoinDerivationInfos.ELECTRUM
// : BitcoinDerivationInfos.LITECOIN,
// );
// if ((Platform.isAndroid || Platform.isIOS) && !isHardwareWallet) {
// await generateInitialMWEBAddresses(
// addressType: SegwitAddressType.mweb,
// seedBytesType: seedBytesType,
// );
// }
// }
await super.init();
} }
@action @action

View file

@ -80,7 +80,7 @@ packages:
description: description:
path: "." path: "."
ref: cake-update-v15 ref: cake-update-v15
resolved-ref: "1a74961e9448d331a2d26099c616b8420a94681c" resolved-ref: "29160733cbc4ef2c7b8c8fe9ed0297c9bffecfe2"
url: "https://github.com/cake-tech/bitcoin_base" url: "https://github.com/cake-tech/bitcoin_base"
source: git source: git
version: "6.1.0" version: "6.1.0"

View file

@ -47,10 +47,10 @@ abstract class BitcoinCashWalletAddressesBase extends ElectrumWalletAddresses wi
); );
return BitcoinCashWalletAddresses( return BitcoinCashWalletAddresses(
walletInfo, electrumJson.walletInfo,
hdWallets: hdWallets, hdWallets: electrumJson.hdWallets,
network: network, network: electrumJson.network,
isHardwareWallet: isHardwareWallet, isHardwareWallet: electrumJson.isHardwareWallet,
initialAddressesRecords: electrumJson.addressesRecords, initialAddressesRecords: electrumJson.addressesRecords,
initialAddressPageType: electrumJson.addressPageType, initialAddressPageType: electrumJson.addressPageType,
initialActiveAddressIndex: electrumJson.activeIndexByType, initialActiveAddressIndex: electrumJson.activeIndexByType,

View file

@ -160,7 +160,7 @@ class CWBitcoin extends Bitcoin {
address: addr.address, address: addr.address,
derivationPath: addr.indexedDerivationPath, derivationPath: addr.indexedDerivationPath,
txCount: addr.txCount, txCount: addr.txCount,
balance: addr.balance, balance: addr.balance.confirmed,
isChange: addr.isChange, isChange: addr.isChange,
isHidden: addr.isHidden || addr.isChange, isHidden: addr.isHidden || addr.isChange,
), ),
@ -537,7 +537,7 @@ class CWBitcoin extends Bitcoin {
address: addr.address, address: addr.address,
derivationPath: addr.indexedDerivationPath, derivationPath: addr.indexedDerivationPath,
txCount: addr.txCount, txCount: addr.txCount,
balance: addr.balance, balance: addr.balance.confirmed,
isChange: addr.isChange, isChange: addr.isChange,
isHidden: addr.isHidden || addr.isChange, isHidden: addr.isHidden || addr.isChange,
)) ))
@ -554,7 +554,7 @@ class CWBitcoin extends Bitcoin {
address: addr.address, address: addr.address,
derivationPath: addr.indexedDerivationPath, derivationPath: addr.indexedDerivationPath,
txCount: addr.txCount, txCount: addr.txCount,
balance: addr.balance, balance: addr.balance.confirmed,
isChange: addr.isChange, isChange: addr.isChange,
isHidden: addr.isHidden || addr.isChange, isHidden: addr.isHidden || addr.isChange,
)) ))

View file

@ -311,7 +311,6 @@ class CakePayBuyCardDetailPage extends BasePage {
} catch (_) { } catch (_) {
await cakePayPurchaseViewModel.cakePayService.logout(); await cakePayPurchaseViewModel.cakePayService.logout();
} }
} }
cakePayPurchaseViewModel.isPurchasing = false; cakePayPurchaseViewModel.isPurchasing = false;
} }
@ -390,10 +389,13 @@ class CakePayBuyCardDetailPage extends BasePage {
currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency, currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(popupContext).send_amount, amount: S.of(popupContext).send_amount,
amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted, amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted,
fiatAmountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, fiatAmountValue:
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(popupContext).send_fee, fee: S.of(popupContext).send_fee,
feeRate: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeRate,
feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted, feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, feeFiatAmount:
cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: cakePayPurchaseViewModel.sendViewModel.outputs, outputs: cakePayPurchaseViewModel.sendViewModel.outputs,
onSlideComplete: () async { onSlideComplete: () async {
Navigator.of(popupContext).pop(); Navigator.of(popupContext).pop();

View file

@ -22,6 +22,7 @@ class SyncIndicator extends StatelessWidget {
final syncIndicatorWidth = 237.0; final syncIndicatorWidth = 237.0;
final status = dashboardViewModel.status; final status = dashboardViewModel.status;
final statusText = syncStatusTitle(status); final statusText = syncStatusTitle(status);
final isScanning = status is SyncingSyncStatus;
final progress = status.progress(); final progress = status.progress();
final indicatorOffset = progress * syncIndicatorWidth; final indicatorOffset = progress * syncIndicatorWidth;
final indicatorWidth = progress < 1 final indicatorWidth = progress < 1
@ -64,7 +65,17 @@ class SyncIndicator extends StatelessWidget {
SyncIndicatorIcon(isSynced: status is SyncedSyncStatus), SyncIndicatorIcon(isSynced: status is SyncedSyncStatus),
Padding( Padding(
padding: EdgeInsets.only(left: 6), padding: EdgeInsets.only(left: 6),
child: RollingText(statusText), child: isScanning
? Text(
statusText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color:
Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
),
)
: RollingText(statusText),
) )
], ],
), ),

View file

@ -214,7 +214,6 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
_exchangeStateReaction = reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state, _exchangeStateReaction = reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state,
(ExecutionState state) async { (ExecutionState state) async {
if (dialogContext != null && dialogContext?.mounted == true) { if (dialogContext != null && dialogContext?.mounted == true) {
Navigator.of(dialogContext!).pop(); Navigator.of(dialogContext!).pop();
} }
@ -289,6 +288,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
fee: isEVMCompatibleChain(widget.exchangeTradeViewModel.sendViewModel.walletType) fee: isEVMCompatibleChain(widget.exchangeTradeViewModel.sendViewModel.walletType)
? S.of(bottomSheetContext).send_estimated_fee ? S.of(bottomSheetContext).send_estimated_fee
: S.of(bottomSheetContext).send_fee, : S.of(bottomSheetContext).send_fee,
feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate,
feeValue: feeValue:
widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeFormatted, widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel

View file

@ -567,6 +567,7 @@ class SendPage extends BasePage {
fee: isEVMCompatibleChain(sendViewModel.walletType) fee: isEVMCompatibleChain(sendViewModel.walletType)
? S.of(bottomSheetContext).send_estimated_fee ? S.of(bottomSheetContext).send_estimated_fee
: S.of(bottomSheetContext).send_fee, : S.of(bottomSheetContext).send_fee,
feeRate: sendViewModel.pendingTransaction!.feeRate,
feeValue: sendViewModel.pendingTransaction!.feeFormatted, feeValue: sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: sendViewModel.outputs, outputs: sendViewModel.outputs,
@ -604,8 +605,7 @@ class SendPage extends BasePage {
context: context, context: context,
isDismissible: false, isDismissible: false,
builder: (BuildContext bottomSheetContext) { builder: (BuildContext bottomSheetContext) {
return showContactSheet && return showContactSheet && sendViewModel.ocpRequest == null
sendViewModel.ocpRequest == null
? InfoBottomSheet( ? InfoBottomSheet(
currentTheme: currentTheme, currentTheme: currentTheme,
showDontAskMeCheckbox: true, showDontAskMeCheckbox: true,

View file

@ -119,7 +119,6 @@ class RBFDetailsPage extends BasePage {
} }
reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) { reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) {
if (state is! IsExecutingState && if (state is! IsExecutingState &&
loadingBottomSheetContext != null && loadingBottomSheetContext != null &&
loadingBottomSheetContext!.mounted) { loadingBottomSheetContext!.mounted) {
@ -191,14 +190,20 @@ class RBFDetailsPage extends BasePage {
titleText: S.of(bottomSheetContext).confirm_transaction, titleText: S.of(bottomSheetContext).confirm_transaction,
currentTheme: currentTheme, currentTheme: currentTheme,
walletType: transactionDetailsViewModel.sendViewModel.walletType, walletType: transactionDetailsViewModel.sendViewModel.walletType,
titleIconPath: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, titleIconPath:
transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency,
amount: S.of(bottomSheetContext).send_amount, amount: S.of(bottomSheetContext).send_amount,
amountValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted, amountValue:
fiatAmountValue: transactionDetailsViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted,
fiatAmountValue: transactionDetailsViewModel
.sendViewModel.pendingTransactionFiatAmountFormatted,
fee: S.of(bottomSheetContext).send_fee, fee: S.of(bottomSheetContext).send_fee,
feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, feeRate: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeRate,
feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, feeValue:
transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: transactionDetailsViewModel
.sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: transactionDetailsViewModel.sendViewModel.outputs, outputs: transactionDetailsViewModel.sendViewModel.outputs,
onSlideComplete: () async { onSlideComplete: () async {
Navigator.of(bottomSheetContext).pop(); Navigator.of(bottomSheetContext).pop();

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/base_page.dart';
@ -44,7 +45,12 @@ class TransactionDetailsPage extends BasePage {
if (item is StandartListItem) { if (item is StandartListItem) {
Widget? addressTextWidget; Widget? addressTextWidget;
if (item.title.toLowerCase() == 'recipient addresses' || final isContactAddress = item.value !=
AddressResolver.extractAddressByType(
raw: item.value,
type: transactionDetailsViewModel.wallet.currency,
);
if (!isContactAddress && item.title.toLowerCase() == 'recipient addresses' ||
item.title.toLowerCase() == 'source address') { item.title.toLowerCase() == 'source address') {
addressTextWidget = getFormattedAddress( addressTextWidget = getFormattedAddress(
context: context, context: context,
@ -96,8 +102,10 @@ class TransactionDetailsPage extends BasePage {
child: SelectButton( child: SelectButton(
text: S.of(context).bump_fee, text: S.of(context).bump_fee,
onTap: () async { onTap: () async {
Navigator.of(context).pushNamed(Routes.bumpFeePage, Navigator.of(context).pushNamed(Routes.bumpFeePage, arguments: [
arguments: [transactionDetailsViewModel.transactionInfo, transactionDetailsViewModel.rawTransaction]); transactionDetailsViewModel.transactionInfo,
transactionDetailsViewModel.rawTransaction
]);
}, },
), ),
); );
@ -124,17 +132,9 @@ class TransactionDetailsPage extends BasePage {
final bool hasDoubleNewline = value.contains('\n\n'); final bool hasDoubleNewline = value.contains('\n\n');
if (hasDoubleNewline) { if (hasDoubleNewline) {
final blocks = value final blocks = value.split('\n\n').map((b) => b.trim()).where((b) => b.isNotEmpty).toList();
.split('\n\n')
.map((b) => b.trim())
.where((b) => b.isNotEmpty)
.toList();
for (final block in blocks) { for (final block in blocks) {
final lines = block final lines = block.split('\n').map((l) => l.trim()).where((l) => l.isNotEmpty).toList();
.split('\n')
.map((l) => l.trim())
.where((l) => l.isNotEmpty)
.toList();
if (lines.length > 1) { if (lines.length > 1) {
children.add(Text(lines.first, style: textStyle)); children.add(Text(lines.first, style: textStyle));
for (int i = 1; i < lines.length; i++) { for (int i = 1; i < lines.length; i++) {
@ -158,11 +158,7 @@ class TransactionDetailsPage extends BasePage {
children.add(SizedBox(height: 8)); children.add(SizedBox(height: 8));
} }
} else { } else {
final lines = value final lines = value.split('\n').map((l) => l.trim()).where((l) => l.isNotEmpty).toList();
.split('\n')
.map((l) => l.trim())
.where((l) => l.isNotEmpty)
.toList();
bool firstLineIsContactName = (lines.length > 1 && lines.first.length < 20); bool firstLineIsContactName = (lines.length > 1 && lines.first.length < 20);
int startIndex = 0; int startIndex = 0;
if (firstLineIsContactName) { if (firstLineIsContactName) {

View file

@ -24,6 +24,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
final String amountValue; final String amountValue;
final String fiatAmountValue; final String fiatAmountValue;
final String fee; final String fee;
final String? feeRate;
final String feeValue; final String feeValue;
final String feeFiatAmount; final String feeFiatAmount;
final List<Output> outputs; final List<Output> outputs;
@ -44,6 +45,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
required this.amountValue, required this.amountValue,
required this.fiatAmountValue, required this.fiatAmountValue,
required this.fee, required this.fee,
this.feeRate,
required this.feeValue, required this.feeValue,
required this.feeFiatAmount, required this.feeFiatAmount,
required this.outputs, required this.outputs,
@ -119,6 +121,16 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
itemSubTitleTextStyle: itemSubTitleTextStyle, itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor, tileBackgroundColor: tileBackgroundColor,
), ),
if (feeRate != null && feeRate!.isNotEmpty) ...[
const SizedBox(height: 8),
StandardTile(
itemTitle: S.current.send_estimated_fee,
itemValue: "$feeRate sat/byte",
itemTitleTextStyle: itemTitleTextStyle,
itemSubTitleTextStyle: itemSubTitleTextStyle,
tileBackgroundColor: tileBackgroundColor,
),
],
const SizedBox(height: 8), const SizedBox(height: 8),
Column( Column(
children: [ children: [
@ -237,7 +249,7 @@ class StandardTile extends StatelessWidget {
required this.itemValue, required this.itemValue,
required this.itemTitleTextStyle, required this.itemTitleTextStyle,
this.itemSubTitle, this.itemSubTitle,
required this.itemSubTitleTextStyle, this.itemSubTitleTextStyle,
required this.tileBackgroundColor, required this.tileBackgroundColor,
}); });
@ -245,7 +257,7 @@ class StandardTile extends StatelessWidget {
final String itemValue; final String itemValue;
final TextStyle itemTitleTextStyle; final TextStyle itemTitleTextStyle;
final String? itemSubTitle; final String? itemSubTitle;
final TextStyle itemSubTitleTextStyle; final TextStyle? itemSubTitleTextStyle;
final Color tileBackgroundColor; final Color tileBackgroundColor;
@override @override
@ -267,7 +279,7 @@ class StandardTile extends StatelessWidget {
Text(itemValue, style: itemTitleTextStyle), Text(itemValue, style: itemTitleTextStyle),
itemSubTitle == null itemSubTitle == null
? Container() ? Container()
: Text(itemSubTitle!, style: itemSubTitleTextStyle), : Text(itemSubTitle!, style: itemSubTitleTextStyle!),
], ],
), ),
], ],
@ -388,31 +400,21 @@ class AddressExpansionTile extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
AddressFormatter.buildSegmentedAddress( Text(
address: address, isBatchSending ? name : contactType,
walletType: walletType, style: itemTitleTextStyle,
evenTextStyle: TextStyle( softWrap: true,
fontSize: 12, ),
Text(
isBatchSending ? amount : name,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato', fontFamily: 'Lato',
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none, decoration: TextDecoration.none,
), ),
), ),
if (stealthAddressText(stealthAddress) != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: AddressFormatter.buildSegmentedAddress(
address: stealthAddressText(stealthAddress)!,
walletType: walletType,
evenTextStyle: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none),
),
),
], ],
), ),
), ),
@ -422,15 +424,38 @@ class AddressExpansionTile extends StatelessWidget {
Row( Row(
children: [ children: [
Expanded( Expanded(
child: AddressFormatter.buildSegmentedAddress( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (stealthAddress != null)
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"Stealth Address:",
style: itemSubTitleTextStyle,
),
),
AddressFormatter.buildSegmentedAddress(
address: address, address: address,
walletType: walletType, walletType: walletType,
evenTextStyle: TextStyle( evenTextStyle: itemSubTitleTextStyle,
fontSize: 12, ),
fontFamily: 'Lato', if (stealthAddress != null) ...[
fontWeight: FontWeight.w600, Padding(
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, padding: const EdgeInsets.symmetric(vertical: 8.0),
decoration: TextDecoration.none)), child: Text(
"Generated Address:",
style: itemSubTitleTextStyle,
),
),
AddressFormatter.buildSegmentedAddress(
address: stealthAddress!,
walletType: walletType,
evenTextStyle: itemSubTitleTextStyle,
),
]
],
),
), ),
], ],
), ),
@ -442,11 +467,3 @@ class AddressExpansionTile extends StatelessWidget {
); );
} }
} }
String? stealthAddressText(String? stealthAddress) {
if (stealthAddress == null) {
return null;
}
return stealthAddress.isNotEmpty ? "-> $stealthAddress" : null;
}