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

View file

@ -3,10 +3,10 @@ import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.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_transaction_credentials.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/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
@ -62,23 +62,17 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
required super.unspentCoinsInfo,
required super.encryptionFileUtils,
required super.hdWallets,
required super.network,
super.mnemonic,
super.xpub,
BasedUtxoNetwork? networkParam,
super.initialBalance,
super.passphrase,
bool? alwaysScan,
super.initialUnspentCoins,
bool? alwaysScan,
Map<String, dynamic>? walletAddressesSnapshot,
}) : _alwaysScan = alwaysScan ?? false,
super(
network: networkParam == null
? BitcoinNetwork.mainnet
: networkParam == BitcoinNetwork.mainnet
? BitcoinNetwork.mainnet
: BitcoinNetwork.testnet,
currency:
networkParam == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
currency: network == BitcoinNetwork.testnet ? CryptoCurrency.tbtc : CryptoCurrency.btc,
) {
if (walletAddressesSnapshot != null) {
walletAddresses = BitcoinWalletAddressesBase.fromJson(
@ -91,7 +85,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
} else {
this.walletAddresses = BitcoinWalletAddresses(
walletInfo,
network: networkParam ?? network,
network: network,
isHardwareWallet: isHardwareWallet,
hdWallets: hdWallets,
);
@ -114,9 +108,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
}
@override
Future<bool?> initAddresses([bool? sync]) async {
var isDiscovered = await super.initAddresses(sync);
bool? discovered;
Future<InitAddressesData> initAddresses([bool? sync]) async {
final initData = await super.initAddresses(sync);
var isDiscovered = initData.isDiscovered;
var discovered = initData.discovered;
// NOTE: will initiate by priority from the first walletAddressTypes
// 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)
initAddresses(true);
else if (isDiscovered == true)
syncStatus = SyncedSyncStatus();
else if (isDiscovered == false && discovered == false) initAddresses(sync);
return isDiscovered;
return InitAddressesData(
isDiscovered: isDiscovered,
discovered: discovered,
);
}
static Future<BitcoinWallet> create({
@ -211,9 +207,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
String? passphrase,
BasedUtxoNetwork? network,
}) async {
network = network == null
? BitcoinNetwork.mainnet
: network == BitcoinNetwork.mainnet
? BitcoinNetwork.mainnet
: BitcoinNetwork.testnet;
final hdWallets = await ElectrumWalletBase.getAccountHDWallets(
walletInfo: walletInfo,
network: network ?? BitcoinNetwork.mainnet,
network: network,
mnemonic: mnemonic,
passphrase: passphrase,
);
@ -225,7 +227,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
encryptionFileUtils: encryptionFileUtils,
networkParam: network,
network: network,
hdWallets: hdWallets,
);
}
@ -297,7 +299,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
unspentCoinsInfo: unspentCoinsInfo,
initialBalance: snp?.balance,
encryptionFileUtils: encryptionFileUtils,
networkParam: network,
network: network,
alwaysScan: snp?.alwaysScan,
initialUnspentCoins: snp?.unspentCoins,
walletAddressesSnapshot: snp?.walletAddressesSnapshot,
@ -531,11 +533,11 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
addressRecord.txCount = 0;
addressRecord.balance = 0;
addressRecord.balance = ElectrumBalance.zero();
});
walletAddresses.receivedSPAddresses.forEach((addressRecord) {
addressRecord.txCount = 0;
addressRecord.balance = 0;
addressRecord.balance = ElectrumBalance.zero();
});
final silentPaymentWallet = walletAddresses.silentPaymentWallet;
@ -560,13 +562,13 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
if (addressRecord.address == silentPaymentAddress.toAddress(network)) {
addressRecord.txCount += 1;
addressRecord.balance += unspent.value;
addressRecord.balance.confirmed += unspent.value;
}
});
walletAddresses.receivedSPAddresses.forEach((addressRecord) {
if (addressRecord.address == receiveAddressRecord.address) {
addressRecord.txCount += 1;
addressRecord.balance += unspent.value;
addressRecord.balance.confirmed += unspent.value;
}
});
}
@ -576,28 +578,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
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
@action
Future<void> addCoinInfo(BitcoinUnspent coin) async {
@ -620,6 +600,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
);
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) {
walletAddresses.silentPaymentAddresses.forEach((addressRecord) {
addressRecord.txCount = 0;
addressRecord.balance = 0;
addressRecord.balance = ElectrumBalance.zero();
});
walletAddresses.receivedSPAddresses.forEach((addressRecord) {
addressRecord.txCount = 0;
addressRecord.balance = 0;
addressRecord.balance = ElectrumBalance.zero();
});
for (final map in result.transactions!.entries) {
@ -693,7 +676,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
existingTxInfo.amount = tx.amount;
existingTxInfo.confirmations = tx.confirmations;
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
.where(
(unspent) => !unspentCoins.any((element) =>
@ -710,29 +698,27 @@ abstract class BitcoinWalletBase extends ElectrumWallet<BitcoinWalletAddresses>
unspentCoins.forEach(updateCoin);
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
transactionHistory.addOne(existingTxInfo);
// Update balance record
balance[currency]!.confirmed += newAmount;
}
// Updates existing TX
transactionHistory.addOne(existingTxInfo);
} else {
// else: First time seeing this TX after scanning
unspentCoins.forEach(_updateSilentAddressRecord);
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;
} else if (!isHardwareWallet) {
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));

View file

@ -14,6 +14,8 @@ part 'bitcoin_wallet_addresses.g.dart';
class BitcoinWalletAddresses = BitcoinWalletAddressesBase with _$BitcoinWalletAddresses;
abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with Store {
static const _OLD_SP_PATH = "m/352'/1'/0'/#'/0";
BitcoinWalletAddressesBase(
WalletInfo walletInfo, {
required super.hdWallets,
@ -22,7 +24,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
super.initialAddressesRecords,
super.initialActiveAddressIndex,
super.initialAddressPageType,
this.loadedFromNewSnapshot = false,
List<BitcoinSilentPaymentAddressRecord>? initialSilentAddresses,
List<BitcoinReceivedSPAddressRecord>? initialReceivedSPAddresses,
}) : silentPaymentAddresses = ObservableList<BitcoinSilentPaymentAddressRecord>.of(
@ -36,10 +37,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
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.
@override
final walletAddressTypes = [
@ -50,6 +47,8 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
SegwitAddressType.p2wsh,
];
List<BaseBitcoinAddressRecord> get otherAddresses => silentPaymentAddresses;
@observable
SilentPaymentOwner? silentPaymentWallet;
final ObservableList<BitcoinSilentPaymentAddressRecord> silentPaymentAddresses;
@ -77,7 +76,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
Future<void> generateInitialSPAddresses() async {
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) {
final oldScanPath = Bip32PathParser.parse(_OLD_SP_PATH.replaceFirst("#", "1"));
final oldSpendPath = Bip32PathParser.parse(_OLD_SP_PATH.replaceFirst("#", "0"));
@ -255,43 +254,27 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
@override
@action
Future<void> updateAddressesInBox() async {
// receiveAddressesMapped.entries.forEach((e) {
// final addressType = e.key;
// final addresses = e.value;
await super.updateAddressesInBox();
// for (final addr in addresses) {
// if (getIsReceive(addr)) {
// allAddressesMap[addr.address] = addr.name;
final addr = activeSilentAddress ??
silentPaymentAddresses.firstWhereOrNull(
(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 addressString =
// '${addr.address.substring(0, 9 + 5)}...${addr.address.substring(addr.address.length - 9, addr.address.length)}';
final isCurrentType = addressPageType == SilentPaymentsAddresType.p2sp;
// if (!isCurrentType) {
// addressesMap[addr.address] = addr.name.isEmpty
// ? "Silent Payments" + ': $addressString'
// : "Silent Payments - " + addr.name + ': $addressString';
// } else {
// addressesMap[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;
// }
// }
// }
// });
if (!isCurrentType) {
addressesMap[addr.address] = addr.name.isEmpty
? "Silent Payments" + ': $addressString'
: "Silent Payments - " + addr.name + ': $addressString';
} else {
addressesMap[addr.address] = 'Active - Silent Payments' + ': $addressString';
}
}
await saveAddressesInBox();
}
@ -332,6 +315,16 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
addressesSet.addAll(addresses);
this.receivedSPAddresses.clear();
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
@ -403,7 +396,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
silentPaymentAddresses.map((address) => address.toJSON()).toList();
json['receivedSPAddresses'] = receivedSPAddresses.map((address) => address.toJSON()).toList();
json['silentAddressIndex'] = silentAddressIndex.toString();
json['loadedFromNewSnapshot'] = true;
return json;
}
@ -475,7 +467,6 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
initialActiveAddressIndex: electrumJson.activeIndexByType,
initialSilentAddresses: initialSilentAddresses,
initialReceivedSPAddresses: initialReceivedSPAddresses,
loadedFromNewSnapshot: snp['loadedFromNewSnapshot'] as bool? ?? false,
);
}
}

View file

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

View file

@ -16,6 +16,20 @@ class ElectrumBalance extends Balance {
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) {
if (jsonSource == null) {
return null;
@ -61,8 +75,8 @@ class ElectrumBalance extends Balance {
BitcoinAmountUtils.bitcoinAmountToString(amount: secondUnconfirmed);
@override
String get formattedFullAvailableBalance =>
BitcoinAmountUtils.bitcoinAmountToString(amount: (confirmed + unconfirmed) + secondConfirmed - frozen);
String get formattedFullAvailableBalance => BitcoinAmountUtils.bitcoinAmountToString(
amount: (confirmed + unconfirmed) + secondConfirmed - frozen);
String toJSON() => json.encode({
'confirmed': confirmed,

View file

@ -1,6 +1,7 @@
import 'dart:convert';
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_core/transaction_direction.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(
ElectrumTransactionBundle bundle,
WalletType type,
@ -254,9 +305,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
final weSent = ourSentAmounts.length > 0;
final weReceived = ourReceivedAmounts.length > 0;
final weReceivedAll = ourReceivedAmounts.length == bundle.originalTransaction.outputs.length;
if (ourReceivedAmounts.length == bundle.originalTransaction.outputs.length) {
// All outputs in this tx were received
if (weReceivedAll) {
direction = TransactionDirection.incoming;
amount = ourTotalReceivedAmount;
} else if (weSent && weReceived && ourTotalSentAmount > ourTotalReceivedAmount) {
@ -277,7 +328,6 @@ class ElectrumTransactionInfo extends TransactionInfo {
}
final fee = totalInputsAmount - totalOutsAmount;
amount = amount - fee;
return ElectrumTransactionInfo(
type,

View file

@ -271,7 +271,7 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
BasedUtxoNetwork network;
// TODO: networks enum
// TODO: change from isTestnet boolean to a networks enum (regtest, signet, etc)
@override
bool isTestnet;
@ -306,11 +306,11 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
void Function(FlutterErrorDetails)? _onError;
Timer? _autoSaveTimer;
Timer? _updateFeeRateTimer;
static const int _autoSaveInterval = 1;
Future<bool?> initAddresses([bool? sync]) async {
Future<InitAddressesData> initAddresses([bool? sync]) async {
bool? isDiscovered = null;
bool? discovered;
// NOTE: will initiate by priority from the first walletAddressTypes
// 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) {
break;
}
// else if (sync == true)
// subscribeForStatuses(
// walletAddresses.addressesRecords
// .getRecords(
// addressType: addressType,
// seedBytesType: seedBytesType,
// derivationPath: derivationPath,
// isChange: isChange,
// )
// .whereType<BitcoinAddressRecord>()
// .toList(),
// );
}
if (isDiscovered == false) {
await generateInitialAddresses(
discovered = await generateInitialAddresses(
addressType: addressType,
seedBytesType: walletAddresses.walletSeedBytesType,
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 {
await walletAddresses.init();
await transactionHistory.init();
_autoSaveTimer =
@ -363,13 +385,9 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
syncStatus = SynchronizingSyncStatus();
await subscribeForHeaders(true);
await initAddresses(false);
await updateFeeRates();
_updateFeeRateTimer ??=
Timer.periodic(const Duration(seconds: 5), (timer) async => await updateFeeRates());
await initAddresses(false);
await save();
} catch (e, stacktrace) {
@ -437,7 +455,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
}
}
// TODO: dynamic
int get dustAmount => 546;
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();
Future<BtcTransaction> buildHardwareWalletTransaction({
@ -1031,6 +1047,7 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
address = fakePublicKey.toP2wshAddress();
break;
case SegwitAddressType.p2tr:
case SilentPaymentsAddresType.p2sp:
address = fakePublicKey.toTaprootAddress();
break;
case SegwitAddressType.mweb:
@ -1115,7 +1132,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
_receivePort?.close();
} catch (_) {}
_autoSaveTimer?.cancel();
_updateFeeRateTimer?.cancel();
}
@action
@ -1125,6 +1141,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
]) async {
scripthashes ??= walletAddresses.allScriptHashes;
if (scripthashes.isEmpty) {
return null;
}
if (wait == true) {
return ElectrumWorkerListUnspentResponse.fromJson(
await waitSendWorker(
@ -1163,8 +1183,8 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
final updatedUnspentCoins = <BitcoinUnspent>[];
await Future.wait(unspents.entries.map((entry) async {
final unspent = entry.value;
final scriptHash = entry.key;
final unspent = entry.value;
final addressRecord = walletAddresses.allAddresses.firstWhereOrNull(
(element) => element.scriptHash == scriptHash,
@ -1174,12 +1194,19 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
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 {
final coin = BitcoinUnspent.fromJSON(addressRecord, unspent.toJson());
coin.isChange = addressRecord.isChange;
// <-
// TODO: \/
final tx = await fetchTransactionInfo(hash: coin.hash);
final tx = await getExistingTransaction(hash: coin.hash);
if (tx != null) {
coin.confirmations = tx.confirmations;
}
@ -1194,7 +1221,6 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
await refreshUnspentCoinsInfo();
}
// TODO: move to ElectrumUnspents
@action
Future<void> addCoinInfo(BitcoinUnspent coin) async {
// 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 {
if (tx.confirmations > 0) return null;
try {
final bundle = await getTransactionExpanded(hash: tx.txHash);
_updateInputsAndOutputs(tx, bundle);
tx.updateInputsAndOutputs(bundle, network);
if (bundle.confirmations > 0) return null;
return bundle.originalTransaction.canReplaceByFee ? bundle.originalTransaction.toHex() : null;
} catch (e) {
@ -1631,6 +1659,12 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
).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 {
try {
return ElectrumTransactionInfo.fromElectrumBundle(
@ -1694,6 +1728,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
addressByScripthashes[addressRecord.scriptHash] = addressRecord.address;
});
if (scripthashByAddress.isEmpty || addressByScripthashes.isEmpty) {
return null;
}
if (wait == true)
return ElectrumWorkerScripthashesSubscribeResponse.fromJson(
await waitSendWorker(
@ -1715,10 +1753,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
}
@action
void onBalanceResponse(ElectrumBalance balanceResult) {
void onBalanceResponse(ElectrumGetBalanceResponse balanceResults) {
var totalFrozen = 0;
var totalConfirmed = balanceResult.confirmed;
var totalUnconfirmed = balanceResult.unconfirmed;
var totalConfirmed = 0;
var totalUnconfirmed = 0;
unspentCoins.forInfo(unspentCoinsInfo.values).forEach((unspentCoinInfo) {
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(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
@ -1740,6 +1802,10 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
]) async {
scripthashes ??= walletAddresses.allScriptHashes;
if (scripthashes.isEmpty) {
return null;
}
if (wait == true) {
return ElectrumWorkerGetBalanceResponse.fromJson(
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
Future<void> _onAddressesDiscovered(List<BitcoinAddressRecord> addresses) async {
final scripthashByAddress = await subscribeForStatuses(addresses, true);
@ -2017,70 +2034,40 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
isElectrum: seedBytesType.isElectrum,
);
final existingReceiveAddresses = walletAddresses.addressesRecords.getRecords(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: false,
);
final discoveredExistingReceiveAddresses =
walletAddresses.discoveredAddressesRecord.getIsDiscovered(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: false,
);
if (!discoveredExistingReceiveAddresses &&
existingReceiveAddresses.length < ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT) {
discoverNewAddresses(
for (final isChange in [true, false]) {
final existingAddresses = walletAddresses.addressesRecords.getRecords(
addressType: addressType,
seedBytesType: seedBytesType,
derivationInfo: bitcoinDerivationInfo,
isChange: false,
startIndex: existingReceiveAddresses.length,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: isChange,
);
discovered = true;
} else {
final discoveredExistingAddresses = walletAddresses.discoveredAddressesRecord.getIsDiscovered(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: isChange,
);
final gapCount = isChange
? ElectrumWalletAddressesBase.INITIAL_CHANGE_COUNT
: ElectrumWalletAddressesBase.INITIAL_RECEIVE_COUNT;
if (!discoveredExistingAddresses && existingAddresses.length < gapCount) {
discoverNewAddresses(
addressType: addressType,
seedBytesType: seedBytesType,
derivationInfo: bitcoinDerivationInfo,
isChange: isChange,
startIndex: existingAddresses.length,
);
discovered = true;
}
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(
addressType: addressType,
seedBytesType: seedBytesType,
derivationPath: bitcoinDerivationInfo.derivationPath.toString(),
isChange: true,
isChange: isChange,
discovered: true,
);
}
@ -2119,8 +2106,7 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
needsToDiscover = recordList.sublist(recordList.length - countToDiscover).any(
(record) {
return scripthashStatuses?.any(
(scripthashStatus) =>
scripthashStatus.scripthash == (record as BitcoinAddressRecord).scriptHash,
(scripthashStatus) => scripthashStatus.scripthash == record.scriptHash,
) ??
record.getIsUsed();
},
@ -2146,6 +2132,12 @@ abstract class ElectrumWalletBase<T extends ElectrumWalletAddresses>
).toJson(),
);
}
@override
String formatCryptoAmount(String amount) {
final amountInt = int.parse(amount);
return BitcoinAmountUtils.bitcoinAmountToString(amount: amountInt);
}
}
class ElectrumEstimatedTx {
@ -2255,3 +2247,13 @@ class CreateTxData {
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 xpriv => hdWallet.privateKey.toExtended;
// NOTE: order matters in priority
List<SeedBytesType> get seedBytesTypes {
final seedBytesTypes = <SeedBytesType>[];
// NOTE: order matters in priority
if (hdWallets.containsKey(SeedBytesType.bip39)) {
seedBytesTypes.add(SeedBytesType.bip39);
}
@ -84,7 +84,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@computed
BitcoinDerivationInfo get _defaultAddressPageDerivationInfo =>
BitcoinDerivationInfo get defaultAddressPageDerivationInfo =>
BitcoinAddressUtils.getDerivationFromType(
addressPageType,
network: network,
@ -100,8 +100,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@observable
BitcoinAddressRecordMap addressesRecords;
Set<String> get hiddenAddresses =>
allAddresses.where((address) => address.isHidden).map((address) => address.address).toSet();
@observable
Set<String> _hiddenAddresses = {};
@computed
Set<String> get hiddenAddresses => _hiddenAddresses;
List<BaseBitcoinAddressRecord> get otherAddresses => [];
@observable
List<BitcoinAddressRecord> _allAddresses = [];
@ -110,32 +114,53 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
List<BitcoinAddressRecord> updateAllAddresses() {
_allAddresses = addressesRecords.allRecords().whereType<BitcoinAddressRecord>().toList();
_allAddresses = addressesRecords.allRecords().toList();
updateAllScriptHashes();
updateSelectedReceiveAddresses();
updateSelectedChangeAddresses();
allAddressesMap.clear();
_allAddresses.forEach((addressRecord) {
final hiddenAddresses = <String>{};
final allScriptHashes = <String>[];
for (final addressRecord in allAddresses) {
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;
}
@observable
// NOTE: Selected Receive Addresses = Receive addresses selected by current receive page type
List<BaseBitcoinAddressRecord> _selectedReceiveAddresses = [];
List<BitcoinAddressRecord> _selectedReceiveAddresses = [];
@computed
List<BaseBitcoinAddressRecord> get selectedReceiveAddresses => _selectedReceiveAddresses;
List<BitcoinAddressRecord> get selectedReceiveAddresses => _selectedReceiveAddresses;
@action
List<BaseBitcoinAddressRecord> updateSelectedReceiveAddresses() {
List<BitcoinAddressRecord> updateSelectedReceiveAddresses() {
_selectedReceiveAddresses = addressesRecords.getRecords(
addressType: addressPageType,
seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(),
derivationPath: defaultAddressPageDerivationInfo.derivationPath.toString(),
isChange: false,
);
updateNextReceiveAddress();
@ -144,17 +169,17 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@observable
// NOTE: Usable Receive Addresses = Receive addresses usable based on auto generate setting
List<BaseBitcoinAddressRecord> _usableReceiveAddresses = [];
List<BitcoinAddressRecord> _usableReceiveAddresses = [];
@computed
List<BaseBitcoinAddressRecord> get usableReceiveAddresses => _usableReceiveAddresses;
List<BitcoinAddressRecord> get usableReceiveAddresses => _usableReceiveAddresses;
@action
List<BaseBitcoinAddressRecord> updateUsableReceiveAddresses() {
List<BitcoinAddressRecord> updateUsableReceiveAddresses() {
_usableReceiveAddresses = addressesRecords
.getRecords(
addressType: addressPageType,
seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(),
derivationPath: defaultAddressPageDerivationInfo.derivationPath.toString(),
isChange: false,
)
.where(
@ -171,32 +196,27 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
BitcoinAddressRecord? updateNextReceiveAddress() {
final receiveAddresses = selectedReceiveAddresses.whereType<BitcoinAddressRecord>();
final receiveAddresses = selectedReceiveAddresses;
if (receiveAddresses.isEmpty) {
return null;
}
_nextReceiveAddress = receiveAddresses.firstWhereOrNull(
(addressRecord) =>
addressRecord.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) &&
!addressRecord.isChange,
) ??
receiveAddresses.first;
_nextReceiveAddress = receiveAddresses.first;
return _nextReceiveAddress;
}
@observable
// NOTE: Selected Change Addresses = Change addresses selected by current receive page type
List<BaseBitcoinAddressRecord> _selectedChangeAddresses = [];
List<BitcoinAddressRecord> _selectedChangeAddresses = [];
@computed
List<BaseBitcoinAddressRecord> get selectedChangeAddresses => _selectedChangeAddresses;
List<BitcoinAddressRecord> get selectedChangeAddresses => _selectedChangeAddresses;
@action
List<BaseBitcoinAddressRecord> updateSelectedChangeAddresses() {
List<BitcoinAddressRecord> updateSelectedChangeAddresses() {
_selectedChangeAddresses = addressesRecords.getRecords(
addressType: addressPageType,
seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(),
derivationPath: defaultAddressPageDerivationInfo.derivationPath.toString(),
isChange: true,
);
updateNextChangeAddress();
@ -206,17 +226,21 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@observable
// NOTE: Usable Receive Addresses = Receive addresses usable based on auto generate setting
List<BaseBitcoinAddressRecord> _usableChangeAddresses = [];
List<BitcoinAddressRecord> _usableChangeAddresses = [];
@computed
List<BaseBitcoinAddressRecord> get usableChangeAddresses => _usableChangeAddresses;
List<BitcoinAddressRecord> get usableChangeAddresses => _usableChangeAddresses;
@action
List<BaseBitcoinAddressRecord> updateUsableChangeAddresses() {
List<BitcoinAddressRecord> updateUsableChangeAddresses() {
_usableChangeAddresses = addressesRecords
.getRecords(
addressType: changeAddressType,
seedBytesType: walletSeedBytesType,
derivationPath: _defaultAddressPageDerivationInfo.derivationPath.toString(),
derivationPath: BitcoinAddressUtils.getDerivationFromType(
changeAddressType,
network: network,
isElectrum: walletSeedBytesType.isElectrum,
).derivationPath.toString(),
isChange: true,
)
.where(
@ -233,17 +257,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
BitcoinAddressRecord? updateNextChangeAddress() {
final changeAddresses = selectedChangeAddresses.whereType<BitcoinAddressRecord>();
final changeAddresses = selectedChangeAddresses;
if (changeAddresses.isEmpty) {
return null;
}
_nextChangeAddress = changeAddresses.firstWhereOrNull(
(addressRecord) =>
addressRecord.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) &&
addressRecord.isChange,
) ??
changeAddresses.first;
_nextChangeAddress = changeAddresses.first;
return _nextChangeAddress;
}
@ -252,18 +271,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@computed
List<String> get allScriptHashes => _allScriptHashes;
@action
List<String> updateAllScriptHashes() {
_allScriptHashes.clear();
_allScriptHashes.addAll(allAddresses.map((address) => address.scriptHash));
return _allScriptHashes;
}
BaseBitcoinAddressRecord getFromAddresses(String address) =>
BitcoinAddressRecord getFromAddresses(String address) =>
allAddresses.firstWhere((element) => element.address == address);
// TODO: feature with toggle to switch change address type
// figure out how to manage on addres page type switch
// TODO: add feature to toggle from settings to switch what change address type is used when sending txs
// or add option that the current change address follows the current addres page type
@observable
BitcoinAddressType changeAddressType = SegwitAddressType.p2wpkh;
@ -273,7 +285,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
void resetActiveAddress() {
try {
final activeReceiveAddresses = selectedReceiveAddresses.whereType<BitcoinAddressRecord>();
final activeReceiveAddresses = selectedReceiveAddresses;
activeBitcoinAddress = activeReceiveAddresses.firstWhereOrNull(
(addressRecord) => addressRecord.index == activeIndexByType[addressPageType],
@ -311,15 +323,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@override
@computed
String get primaryAddress =>
selectedReceiveAddresses
.firstWhereOrNull(
(addressRecord) =>
addressRecord.getIsStillReceiveable(isEnabledAutoGenerateNewAddress) &&
!addressRecord.isChange,
)
?.address ??
'';
String get primaryAddress => nextReceiveAddress?.address ?? '';
Map<BitcoinAddressType, int> activeIndexByType;
@ -342,7 +346,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
isChange: false,
index: newAddressIndex,
addressType: addressPageType,
derivationInfo: _defaultAddressPageDerivationInfo,
derivationInfo: defaultAddressPageDerivationInfo,
hdWallet: hdWallet,
seedBytesType: walletSeedBytesType,
network: network,
@ -433,6 +437,34 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
Future<void> updateAddressesInBox() async {
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';
await saveAddressesInBox();
@ -440,7 +472,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
@action
void updateAddress(String address, String label) {
BaseBitcoinAddressRecord? foundAddress;
BitcoinAddressRecord? foundAddress;
for (final addressRecord in _allAddresses) {
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) {
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 ItemsByDerivationPath<T> = Map<String, ItemsByIsChange<T>>;
@ -572,7 +603,7 @@ class BitcoinAddressRecordMap extends ItemsRecordMap<AddressRecordsBySeedType> {
required SeedBytesType seedBytesType,
required String derivationPath,
required bool isChange,
required List<BaseBitcoinAddressRecord> addressRecords,
required List<BitcoinAddressRecord> addressRecords,
}) {
_data.putIfAbsent(
addressType,
@ -614,18 +645,18 @@ class BitcoinAddressRecordMap extends ItemsRecordMap<AddressRecordsBySeedType> {
_data[addressType]![seedBytesType]![derivationPath]![isChange] = recordsList;
}
List<BaseBitcoinAddressRecord> allRecords() {
List<BitcoinAddressRecord> allRecords() {
return _data.values
.expand((seedTypeMap) => seedTypeMap.values)
.expand((derivationMap) => derivationMap.values)
.expand((changeMap) => changeMap.values)
.fold<List<BaseBitcoinAddressRecord>>(
.fold<List<BitcoinAddressRecord>>(
[],
(acc, records) => acc..addAll(records),
);
}
List<BaseBitcoinAddressRecord> getRecords({
List<BitcoinAddressRecord> getRecords({
required BitcoinAddressType addressType,
required SeedBytesType seedBytesType,
required String derivationPath,
@ -672,7 +703,7 @@ class BitcoinAddressRecordMap extends ItemsRecordMap<AddressRecordsBySeedType> {
(isChange, v) => MapEntry(
isChange == 'true',
(v as List<dynamic>)
.map((address) => BaseBitcoinAddressRecord.fromJSON(address as String))
.map((address) => BitcoinAddressRecord.fromJSON(address as String))
.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 {
_network = request.network;
_walletType = request.walletType;
@ -219,6 +269,9 @@ class ElectrumWorker {
tx.isPending = tx.confirmations == 0;
anyTxWasUpdated = true;
}
} else {
tx.height = newChainTip;
tx.confirmations = 0;
}
});
@ -477,10 +530,11 @@ class ElectrumWorker {
final txInfo = ElectrumTransactionInfo.fromElectrumBundle(
txBundle,
request.walletType,
request.network,
_network!,
addresses: addresses.toSet(),
height: transactionIdsForHeights[hash],
);
txInfo.updateInputsAndOutputs(txBundle, _network!);
request.addresses.forEach(
(addr) {
@ -600,7 +654,7 @@ class ElectrumWorker {
final txInfo = ElectrumTransactionInfo.fromElectrumBundle(
txBundle,
request.walletType,
request.network,
_network!,
addresses: addresses.toSet(),
height: transactionIdsForHeights[hash],
);
@ -758,9 +812,8 @@ class ElectrumWorker {
);
} else if (request.mempoolAPIEnabled) {
try {
date = await getTxDate(
date = await _getTxDate(
hash,
_network!,
request.chainTip,
confirmations: storedTx?.confirmations,
date: storedTx?.date,
@ -788,7 +841,7 @@ class ElectrumWorker {
isDateValidated: date?.isDateValidated,
),
request.walletType,
request.network,
_network!,
addresses: addresses.toSet(),
height: transactionsByIds[hash]?.height,
);
@ -940,9 +993,8 @@ class ElectrumWorker {
);
} else if (request.mempoolAPIEnabled) {
try {
date = await getTxDate(
date = await _getTxDate(
hash,
_network!,
request.chainTip,
confirmations: storedTx?.confirmations,
date: storedTx?.date,
@ -973,7 +1025,7 @@ class ElectrumWorker {
isDateValidated: date?.isDateValidated,
),
request.walletType,
request.network,
_network!,
addresses: addresses.toSet(),
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 {
final scripthashes = request.scripthashes.where((s) => s.isNotEmpty).toList();
final balanceResults = <Map<String, dynamic>>[];
@ -1078,22 +1099,26 @@ class ElectrumWorker {
}));
}
var totalConfirmed = 0;
var totalUnconfirmed = 0;
final balances = <ElectrumBalance>[];
for (final balance in balanceResults) {
final confirmed = balance['confirmed'] as int? ?? 0;
final unconfirmed = balance['unconfirmed'] as int? ?? 0;
totalConfirmed += confirmed;
totalUnconfirmed += unconfirmed;
balances.add(
ElectrumBalance(
confirmed: confirmed,
unconfirmed: unconfirmed,
frozen: 0,
),
);
}
_sendResponse(
ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: totalConfirmed,
unconfirmed: totalUnconfirmed,
frozen: 0,
result: ElectrumGetBalanceResponse(
balances: balances,
scripthashes: scripthashes,
),
id: request.id,
),
@ -1114,9 +1139,7 @@ class ElectrumWorker {
)
.timeout(const Duration(seconds: 3));
if (scriptHashUnspents.isNotEmpty) {
unspents[scriptHash] = scriptHashUnspents;
}
unspents[scriptHash] = scriptHashUnspents;
});
_sendResponse(ElectrumWorkerListUnspentResponse(utxos: unspents, id: request.id));
@ -1337,9 +1360,8 @@ class ElectrumWorker {
if (getTime && _walletType == WalletType.bitcoin) {
if (mempoolAPIEnabled) {
try {
dates = await getTxDate(
dates = await _getTxDate(
hash,
_network!,
currentChainTip,
confirmations: confirmations,
date: date,
@ -1418,7 +1440,7 @@ class ElectrumWorker {
int halfHour = recommendedFees.medium.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
if (hour == halfHour) {
halfHour++;
@ -1566,6 +1588,7 @@ class ElectrumWorker {
final syncingStatus = scanData.isSingleScan
? SyncingSyncStatus(1, 0)
: SyncingSyncStatus.fromHeightValues(scanData.chainTip, initialSyncHeight, syncHeight);
_sendResponse(ElectrumWorkerTweaksSubscribeResponse(
result: TweaksSyncResponse(
height: syncHeight,
@ -1608,11 +1631,7 @@ class ElectrumWorker {
}
// placeholder ElectrumTransactionInfo object to update values based on new scanned unspent(s)
final txDate = await getTxDate(
txid,
scanData.network,
scanData.chainTip,
);
final txDate = await _getTxDate(txid, scanData.chainTip);
final txInfo = ElectrumTransactionInfo(
WalletType.bitcoin,
@ -1651,6 +1670,8 @@ class ElectrumWorker {
);
final labelIndex = labelValue != null ? scanData.labels[label] : 0;
final balance = ElectrumBalance.zero();
balance.confirmed = amount;
final receivedAddressRecord = BitcoinReceivedSPAddressRecord(
receivingOutputAddress,
@ -1661,7 +1682,7 @@ class ElectrumWorker {
isUsed: true,
tweak: t_k,
txCount: 1,
balance: amount,
balance: balance,
spAddress: matchingSPWallet.toAddress(scanData.network),
);
@ -1739,10 +1760,10 @@ class ElectrumWorker {
derivationInfo: request.derivationInfo,
hdWallet: Bip32Slip10Secp256k1.fromExtendedKey(
request.xpriv,
BitcoinAddressUtils.getKeyNetVersion(request.network),
BitcoinAddressUtils.getKeyNetVersion(_network!),
),
seedBytesType: request.seedBytesType,
network: request.network,
network: _network!,
);
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 {
final ElectrumTransactionInfo? tx;
final int height;

View file

@ -44,8 +44,44 @@ class ElectrumWorkerGetBalanceError extends ElectrumWorkerErrorResponse {
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
extends ElectrumWorkerResponse<ElectrumBalance, Map<String, int>?> {
extends ElectrumWorkerResponse<ElectrumGetBalanceResponse, Map<String, dynamic>> {
ElectrumWorkerGetBalanceResponse({
required super.result,
super.error,
@ -54,18 +90,14 @@ class ElectrumWorkerGetBalanceResponse
}) : super(method: ElectrumRequestMethods.getBalance.method);
@override
Map<String, int>? resultJson(result) {
return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed};
Map<String, dynamic> resultJson(result) {
return result.toJson();
}
@override
factory ElectrumWorkerGetBalanceResponse.fromJson(Map<String, dynamic> json) {
return ElectrumWorkerGetBalanceResponse(
result: ElectrumBalance(
confirmed: json['result']['confirmed'] as int,
unconfirmed: json['result']['unconfirmed'] as int,
frozen: 0,
),
result: ElectrumGetBalanceResponse.fromJson(json['result'] as Map<String, dynamic>),
error: json['error'] as String?,
id: json['id'] as int?,
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:
unspentCoins.forEach((coin) {
coin.bitcoinAddressRecord.balance = 0;
coin.bitcoinAddressRecord.balance = ElectrumBalance.zero();
coin.bitcoinAddressRecord.txCount = 0;
});
for (var addressRecord in walletAddresses.mwebAddresses) {
addressRecord.balance = 0;
addressRecord.balance = ElectrumBalance.zero();
addressRecord.txCount = 0;
}
@ -520,7 +520,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
// update the txCount:
addressRecord.txCount++;
addressRecord.balance += utxo.value.toInt();
addressRecord.balance.confirmed += utxo.value.toInt();
addressRecord.setAsUsed();
}
@ -676,7 +676,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
if (!inputAddresses.contains(utxo.address)) {
addressRecord.txCount++;
}
addressRecord.balance -= utxo.value.toInt();
addressRecord.balance.confirmed -= utxo.value.toInt();
amount += utxo.value.toInt();
inputAddresses.add(utxo.address);
input.add(hex.decode(outputId));
@ -855,7 +855,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
} catch (_) {}
for (var addressRecord in walletAddresses.mwebAddresses) {
addressRecord.balance = 0;
addressRecord.balance = ElectrumBalance.zero();
addressRecord.txCount = 0;
}
@ -873,7 +873,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
coin.bitcoinAddressRecord.balance += coinInfo.value;
coin.bitcoinAddressRecord.balance.confirmed += coinInfo.value;
} else {
super.addCoinInfo(coin);
}
@ -1537,7 +1537,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet<LitecoinWalletAddresses
if (!addresses.contains(utxo.address)) {
addresses.add(utxo.address);
}
addressRecord.balance -= utxo.value.toInt();
addressRecord.balance.confirmed -= utxo.value.toInt();
});
transaction.inputAddresses?.addAll(addresses);
printV("isPegIn: $isPegIn, isPegOut: $isPegOut");

View file

@ -67,24 +67,7 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
Future<void> init() async {
if (!super.isHardwareWallet) await initMwebAddresses();
// for (final seedBytesType in hdWallets.keys) {
// 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();
super.init();
}
@action

View file

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

View file

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

View file

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

View file

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

View file

@ -22,6 +22,7 @@ class SyncIndicator extends StatelessWidget {
final syncIndicatorWidth = 237.0;
final status = dashboardViewModel.status;
final statusText = syncStatusTitle(status);
final isScanning = status is SyncingSyncStatus;
final progress = status.progress();
final indicatorOffset = progress * syncIndicatorWidth;
final indicatorWidth = progress < 1
@ -64,7 +65,17 @@ class SyncIndicator extends StatelessWidget {
SyncIndicatorIcon(isSynced: status is SyncedSyncStatus),
Padding(
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,10 +214,9 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
_exchangeStateReaction = reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state,
(ExecutionState state) async {
if (dialogContext != null && dialogContext?.mounted == true) {
Navigator.of(dialogContext!).pop();
}
if (dialogContext != null && dialogContext?.mounted == true) {
Navigator.of(dialogContext!).pop();
}
if (state is! IsExecutingState &&
loadingBottomSheetContext != null &&
@ -289,6 +288,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
fee: isEVMCompatibleChain(widget.exchangeTradeViewModel.sendViewModel.walletType)
? S.of(bottomSheetContext).send_estimated_fee
: S.of(bottomSheetContext).send_fee,
feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate,
feeValue:
widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
final String amountValue;
final String fiatAmountValue;
final String fee;
final String? feeRate;
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
@ -44,6 +45,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
required this.amountValue,
required this.fiatAmountValue,
required this.fee,
this.feeRate,
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
@ -119,6 +121,16 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
itemSubTitleTextStyle: itemSubTitleTextStyle,
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),
Column(
children: [
@ -237,7 +249,7 @@ class StandardTile extends StatelessWidget {
required this.itemValue,
required this.itemTitleTextStyle,
this.itemSubTitle,
required this.itemSubTitleTextStyle,
this.itemSubTitleTextStyle,
required this.tileBackgroundColor,
});
@ -245,7 +257,7 @@ class StandardTile extends StatelessWidget {
final String itemValue;
final TextStyle itemTitleTextStyle;
final String? itemSubTitle;
final TextStyle itemSubTitleTextStyle;
final TextStyle? itemSubTitleTextStyle;
final Color tileBackgroundColor;
@override
@ -267,7 +279,7 @@ class StandardTile extends StatelessWidget {
Text(itemValue, style: itemTitleTextStyle),
itemSubTitle == null
? Container()
: Text(itemSubTitle!, style: itemSubTitleTextStyle),
: Text(itemSubTitle!, style: itemSubTitleTextStyle!),
],
),
],
@ -388,31 +400,21 @@ class AddressExpansionTile extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AddressFormatter.buildSegmentedAddress(
address: address,
walletType: walletType,
evenTextStyle: TextStyle(
fontSize: 12,
Text(
isBatchSending ? name : contactType,
style: itemTitleTextStyle,
softWrap: true,
),
Text(
isBatchSending ? amount : name,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
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(
children: [
Expanded(
child: AddressFormatter.buildSegmentedAddress(
address: address,
walletType: walletType,
evenTextStyle: TextStyle(
fontSize: 12,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none)),
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,
walletType: walletType,
evenTextStyle: itemSubTitleTextStyle,
),
if (stealthAddress != null) ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
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;
}