diff --git a/cw_bitcoin/lib/bitcoin_address_record.dart b/cw_bitcoin/lib/bitcoin_address_record.dart index bb46405e6..3d0ef2bfb 100644 --- a/cw_bitcoin/lib/bitcoin_address_record.dart +++ b/cw_bitcoin/lib/bitcoin_address_record.dart @@ -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) { diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 0917a4b21..db164bd2c 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -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 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? 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 } else { this.walletAddresses = BitcoinWalletAddresses( walletInfo, - network: networkParam ?? network, + network: network, isHardwareWallet: isHardwareWallet, hdWallets: hdWallets, ); @@ -114,9 +108,10 @@ abstract class BitcoinWalletBase extends ElectrumWallet } @override - Future initAddresses([bool? sync]) async { - var isDiscovered = await super.initAddresses(sync); - bool? discovered; + Future 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 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 create({ @@ -211,9 +207,15 @@ abstract class BitcoinWalletBase extends ElectrumWallet 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 walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, encryptionFileUtils: encryptionFileUtils, - networkParam: network, + network: network, hdWallets: hdWallets, ); } @@ -297,7 +299,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet 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 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 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 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 addCoinInfo(BitcoinUnspent coin) async { @@ -620,6 +600,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet ); 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 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 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 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 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)); diff --git a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart index 46bbd640f..4de6a260d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_addresses.dart @@ -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? initialSilentAddresses, List? initialReceivedSPAddresses, }) : silentPaymentAddresses = ObservableList.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 get otherAddresses => silentPaymentAddresses; + @observable SilentPaymentOwner? silentPaymentWallet; final ObservableList silentPaymentAddresses; @@ -77,7 +76,7 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S Future 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 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, ); } } diff --git a/cw_bitcoin/lib/bitcoin_wallet_service.dart b/cw_bitcoin/lib/bitcoin_wallet_service.dart index 4aeb20739..87ff65f5b 100644 --- a/cw_bitcoin/lib/bitcoin_wallet_service.dart +++ b/cw_bitcoin/lib/bitcoin_wallet_service.dart @@ -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, ); diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index a035393c1..18cddd820 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -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, diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index 32f1581c7..56cf3981e 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -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 = []; + final tempOutputAddresses = []; + + 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, diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index b1fd99839..d80857758 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -271,7 +271,7 @@ abstract class ElectrumWalletBase 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 void Function(FlutterErrorDetails)? _onError; Timer? _autoSaveTimer; - Timer? _updateFeeRateTimer; static const int _autoSaveInterval = 1; - Future initAddresses([bool? sync]) async { + Future 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 if (isDiscovered == false) { break; } + // else if (sync == true) + // subscribeForStatuses( + // walletAddresses.addressesRecords + // .getRecords( + // addressType: addressType, + // seedBytesType: seedBytesType, + // derivationPath: derivationPath, + // isChange: isChange, + // ) + // .whereType() + // .toList(), + // ); } if (isDiscovered == false) { - await generateInitialAddresses( + discovered = await generateInitialAddresses( addressType: addressType, seedBytesType: walletAddresses.walletSeedBytesType, bitcoinDerivationInfo: bitcoinDerivationInfo, @@ -346,10 +358,20 @@ abstract class ElectrumWalletBase } } - 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 init() async { + await walletAddresses.init(); await transactionHistory.init(); _autoSaveTimer = @@ -363,13 +385,9 @@ abstract class ElectrumWalletBase 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 } } - // TODO: dynamic int get dustAmount => 546; bool isBelowDust(int amount) => amount <= dustAmount; @@ -930,7 +947,6 @@ abstract class ElectrumWalletBase } } - // TODO: LedgerElectrumWallet void setLedgerConnection(ledger.LedgerConnection connection) => throw UnimplementedError(); Future buildHardwareWalletTransaction({ @@ -1031,6 +1047,7 @@ abstract class ElectrumWalletBase address = fakePublicKey.toP2wshAddress(); break; case SegwitAddressType.p2tr: + case SilentPaymentsAddresType.p2sp: address = fakePublicKey.toTaprootAddress(); break; case SegwitAddressType.mweb: @@ -1115,7 +1132,6 @@ abstract class ElectrumWalletBase _receivePort?.close(); } catch (_) {} _autoSaveTimer?.cancel(); - _updateFeeRateTimer?.cancel(); } @action @@ -1125,6 +1141,10 @@ abstract class ElectrumWalletBase ]) async { scripthashes ??= walletAddresses.allScriptHashes; + if (scripthashes.isEmpty) { + return null; + } + if (wait == true) { return ElectrumWorkerListUnspentResponse.fromJson( await waitSendWorker( @@ -1163,8 +1183,8 @@ abstract class ElectrumWalletBase final updatedUnspentCoins = []; 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 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 await refreshUnspentCoinsInfo(); } - // TODO: move to ElectrumUnspents @action Future addCoinInfo(BitcoinUnspent coin) async { // Check if the coin is already in the unspentCoinsInfo for the wallet @@ -1328,9 +1354,11 @@ abstract class ElectrumWalletBase } Future 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 ).result; } + Future getExistingTransaction({required String hash}) async { + var tx = transactionHistory.transactions.values.firstWhereOrNull((tx) => tx.txHash == hash); + tx ??= await fetchTransactionInfo(hash: hash); + return tx; + } + Future fetchTransactionInfo({required String hash, int? height}) async { try { return ElectrumTransactionInfo.fromElectrumBundle( @@ -1694,6 +1728,10 @@ abstract class ElectrumWalletBase 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 } @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 } }); + 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 ]) async { scripthashes ??= walletAddresses.allScriptHashes; + if (scripthashes.isEmpty) { + return null; + } + if (wait == true) { return ElectrumWorkerGetBalanceResponse.fromJson( await waitSendWorker( @@ -1897,55 +1963,6 @@ abstract class ElectrumWalletBase } } - 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 inputAddresses = []; - List 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 _onAddressesDiscovered(List addresses) async { final scripthashByAddress = await subscribeForStatuses(addresses, true); @@ -2017,70 +2034,40 @@ abstract class ElectrumWalletBase 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 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 ).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, + }); +} diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index 9fcd09e21..29a328543 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -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 get seedBytesTypes { final seedBytesTypes = []; + // 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 get hiddenAddresses => - allAddresses.where((address) => address.isHidden).map((address) => address.address).toSet(); + @observable + Set _hiddenAddresses = {}; + @computed + Set get hiddenAddresses => _hiddenAddresses; + + List get otherAddresses => []; @observable List _allAddresses = []; @@ -110,32 +114,53 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @action List updateAllAddresses() { - _allAddresses = addressesRecords.allRecords().whereType().toList(); + _allAddresses = addressesRecords.allRecords().toList(); - updateAllScriptHashes(); updateSelectedReceiveAddresses(); updateSelectedChangeAddresses(); allAddressesMap.clear(); - _allAddresses.forEach((addressRecord) { + + final hiddenAddresses = {}; + final allScriptHashes = []; + + 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 _selectedReceiveAddresses = []; + List _selectedReceiveAddresses = []; @computed - List get selectedReceiveAddresses => _selectedReceiveAddresses; + List get selectedReceiveAddresses => _selectedReceiveAddresses; @action - List updateSelectedReceiveAddresses() { + List 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 _usableReceiveAddresses = []; + List _usableReceiveAddresses = []; @computed - List get usableReceiveAddresses => _usableReceiveAddresses; + List get usableReceiveAddresses => _usableReceiveAddresses; @action - List updateUsableReceiveAddresses() { + List 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(); + 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 _selectedChangeAddresses = []; + List _selectedChangeAddresses = []; @computed - List get selectedChangeAddresses => _selectedChangeAddresses; + List get selectedChangeAddresses => _selectedChangeAddresses; @action - List updateSelectedChangeAddresses() { + List 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 _usableChangeAddresses = []; + List _usableChangeAddresses = []; @computed - List get usableChangeAddresses => _usableChangeAddresses; + List get usableChangeAddresses => _usableChangeAddresses; @action - List updateUsableChangeAddresses() { + List 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(); + 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 get allScriptHashes => _allScriptHashes; - @action - List 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(); + 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 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 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; +typedef AddressRecords = List; typedef ItemsByIsChange = Map; typedef ItemsByDerivationPath = Map>; @@ -572,7 +603,7 @@ class BitcoinAddressRecordMap extends ItemsRecordMap { required SeedBytesType seedBytesType, required String derivationPath, required bool isChange, - required List addressRecords, + required List addressRecords, }) { _data.putIfAbsent( addressType, @@ -614,18 +645,18 @@ class BitcoinAddressRecordMap extends ItemsRecordMap { _data[addressType]![seedBytesType]![derivationPath]![isChange] = recordsList; } - List allRecords() { + List allRecords() { return _data.values .expand((seedTypeMap) => seedTypeMap.values) .expand((derivationMap) => derivationMap.values) .expand((changeMap) => changeMap.values) - .fold>( + .fold>( [], (acc, records) => acc..addAll(records), ); } - List getRecords({ + List getRecords({ required BitcoinAddressType addressType, required SeedBytesType seedBytesType, required String derivationPath, @@ -672,7 +703,7 @@ class BitcoinAddressRecordMap extends ItemsRecordMap { (isChange, v) => MapEntry( isChange == 'true', (v as List) - .map((address) => BaseBitcoinAddressRecord.fromJSON(address as String)) + .map((address) => BitcoinAddressRecord.fromJSON(address as String)) .toList(), ), ), diff --git a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart index e3545b906..c73ce639c 100644 --- a/cw_bitcoin/lib/electrum_worker/electrum_worker.dart +++ b/cw_bitcoin/lib/electrum_worker/electrum_worker.dart @@ -145,6 +145,56 @@ class ElectrumWorker { } } + Future _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(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 _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 _handleListUnspents(ElectrumWorkerGetBalanceRequest request) async { - // final balanceFutures = >>[]; - - // 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 _handleGetBalance(ElectrumWorkerGetBalanceRequest request) async { final scripthashes = request.scripthashes.where((s) => s.isNotEmpty).toList(); final balanceResults = >[]; @@ -1078,22 +1099,26 @@ class ElectrumWorker { })); } - var totalConfirmed = 0; - var totalUnconfirmed = 0; + final balances = []; 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 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(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; diff --git a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart index 2e4c1208c..9d6b8d16a 100644 --- a/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart +++ b/cw_bitcoin/lib/electrum_worker/methods/get_balance.dart @@ -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 balances; + final List scripthashes; + + Map toJson() { + return { + 'balances': balances + .map((e) => { + 'confirmed': e.confirmed, + 'unconfirmed': e.unconfirmed, + 'frozen': e.frozen, + }) + .toList(), + 'scripthashes': scripthashes, + }; + } + + factory ElectrumGetBalanceResponse.fromJson(Map 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(), + ); + } +} + class ElectrumWorkerGetBalanceResponse - extends ElectrumWorkerResponse?> { + extends ElectrumWorkerResponse> { ElectrumWorkerGetBalanceResponse({ required super.result, super.error, @@ -54,18 +90,14 @@ class ElectrumWorkerGetBalanceResponse }) : super(method: ElectrumRequestMethods.getBalance.method); @override - Map? resultJson(result) { - return {"confirmed": result.confirmed, "unconfirmed": result.unconfirmed}; + Map resultJson(result) { + return result.toJson(); } @override factory ElectrumWorkerGetBalanceResponse.fromJson(Map 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), error: json['error'] as String?, id: json['id'] as int?, completed: json['completed'] as bool? ?? false, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index a04187444..be7082d18 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -450,12 +450,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet 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 diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index ad94ecbf4..ad0bd2c93 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -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" diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart index a61f97c95..f24d3416d 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet_addresses.dart @@ -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, diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index a7dadf49f..70da4ca0f 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -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, )) diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index 51c17bc8b..bc7b14f98 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -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(); diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 05141b738..560d4055e 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -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()!.textColor, + ), + ) + : RollingText(statusText), ) ], ), diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 4b7b8ace9..e7861b7ad 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -214,10 +214,9 @@ class ExchangeTradeState extends State { _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 { 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 diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 8c42f1129..bf26a2938 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -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, diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index ce6ee122b..6f7b5187f 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -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(); diff --git a/lib/src/screens/transaction_details/transaction_details_page.dart b/lib/src/screens/transaction_details/transaction_details_page.dart index c1188d0c7..4928ebaf1 100644 --- a/lib/src/screens/transaction_details/transaction_details_page.dart +++ b/lib/src/screens/transaction_details/transaction_details_page.dart @@ -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) { diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart index ee9109886..65b1c772d 100644 --- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -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 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()!.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()!.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()!.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; -}