diff --git a/cw_bitcoin/lib/litecoin_address_record.dart b/cw_bitcoin/lib/litecoin_address_record.dart new file mode 100644 index 000000000..b967cd05a --- /dev/null +++ b/cw_bitcoin/lib/litecoin_address_record.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; +import 'package:cw_bitcoin/bitcoin_address_record.dart'; + +import 'package:bitcoin_base_old/bitcoin_base.dart'; + +class LitecoinAddressRecord extends BitcoinAddressRecord { + LitecoinAddressRecord( + super.address, { + required super.index, + super.isHidden = false, + super.txCount = 0, + super.balance = 0, + super.name = '', + super.isUsed = false, + required super.type, + String? scriptHash, + required super.network, + }) : scriptHash = scriptHash ?? + (network != null ? BitcoinAddressUtils.scriptHash(address, network: network) : null); + + factory LitecoinAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) { + final decoded = json.decode(jsonSource) as Map; + + return LitecoinAddressRecord( + decoded['address'] as String, + index: decoded['index'] as int, + isHidden: decoded['isHidden'] as bool? ?? false, + isUsed: decoded['isUsed'] as bool? ?? false, + txCount: decoded['txCount'] as int? ?? 0, + name: decoded['name'] as String? ?? '', + balance: decoded['balance'] as int? ?? 0, + type: decoded['type'] != null && decoded['type'] != '' + ? BitcoinAddressType.values + .firstWhere((type) => type.toString() == decoded['type'] as String) + : SegwitAddresType.p2wpkh, + scriptHash: decoded['scriptHash'] as String?, + network: network, + ); + } + + String? scriptHash; + + String getScriptHash(BasedUtxoNetwork network) { + if (scriptHash != null) return scriptHash!; + scriptHash = BitcoinAddressUtils.scriptHash(address, network: network); + return scriptHash!; + } + + @override + String toJSON() => json.encode({ + 'address': address, + 'index': index, + 'isHidden': isHidden, + 'isUsed': isUsed, + 'txCount': txCount, + 'name': name, + 'balance': balance, + 'type': type.toString(), + 'scriptHash': scriptHash, + }); +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 08c56c600..f87383dfb 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -6,15 +6,19 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:crypto/crypto.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cw_bitcoin/exceptions.dart'; +import 'package:cw_bitcoin/litecoin_address_record.dart'; import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/get_height_by_date.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_mweb/mwebd.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:bip39/bip39.dart' as bip39; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base_old/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:blockchain_utils/signer/ecdsa_signing_key.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; @@ -66,8 +70,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { String? xpub, String? passphrase, String? addressPageType, - List? initialAddresses, - List? initialMwebAddresses, + List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex, @@ -151,6 +155,904 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { StreamSubscription? _utxoStream; late bool mwebEnabled; bool processingUtxos = false; + + Bip32Slip10Secp256k1 get hd => accountHD.childKey(Bip32KeyIndex(0)); + + Bip32Slip10Secp256k1 get sideHd => accountHD.childKey(Bip32KeyIndex(1)); + + Set get addressesSet => walletAddresses.allAddresses + .where((element) => element.type != SegwitAddresType.mweb) + .map((addr) => addr.address) + .toSet(); + + List get scriptHashes => walletAddresses.addressesByReceiveType + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) + .map((addr) => (addr as LitecoinAddressRecord).getScriptHash(network)) + .toList(); + + List get publicScriptHashes => walletAddresses.allAddresses + .where((addr) => !addr.isHidden) + .where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress) + .map((addr) => addr.getScriptHash(network)) + .toList(); + + UtxoDetails _createUTXOS({ + required bool sendAll, + required bool paysToSilentPayment, + int credentialsAmount = 0, + int? inputsCount, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) { + List utxos = []; + List vinOutpoints = []; + List inputPrivKeyInfos = []; + final publicKeys = {}; + int allInputsAmount = 0; + bool spendsSilentPayment = false; + bool spendsUnconfirmedTX = false; + + int leftAmount = credentialsAmount; + var availableInputs = unspentCoins.where((utx) { + if (!utx.isSending || utx.isFrozen) { + return false; + } + + switch (coinTypeToSpendFrom) { + case UnspentCoinType.mweb: + return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb; + case UnspentCoinType.nonMweb: + return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb; + case UnspentCoinType.any: + return true; + } + }).toList(); + final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList(); + + // sort the unconfirmed coins so that mweb coins are last: + availableInputs.sort((a, b) => a.bitcoinAddressRecord.type == SegwitAddresType.mweb ? 1 : -1); + + for (int i = 0; i < availableInputs.length; i++) { + final utx = availableInputs[i]; + if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0; + + allInputsAmount += utx.value; + leftAmount = leftAmount - utx.value; + + final address = RegexUtils.addressTypeFromStr(utx.address, network); + ECPrivate? privkey; + bool? isSilentPayment = false; + + final hd = + utx.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd; + + if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) { + final unspentAddress = utx.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord; + privkey = walletAddresses.silentAddress!.b_spend.tweakAdd( + BigintUtils.fromBytes( + BytesUtils.fromHexString(unspentAddress.silentPaymentTweak!), + ), + ); + spendsSilentPayment = true; + isSilentPayment = true; + } else if (!isHardwareWallet) { + privkey = + generateECPrivate(hd: hd, index: utx.bitcoinAddressRecord.index, network: network); + } + + vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout)); + String pubKeyHex; + + if (privkey != null) { + inputPrivKeyInfos.add(ECPrivateInfo( + privkey, + address.type == SegwitAddresType.p2tr, + tweak: !isSilentPayment, + )); + + pubKeyHex = privkey.getPublic().toHex(); + } else { + pubKeyHex = hd.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index)).publicKey.toHex(); + } + + final derivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? electrum_path)}" + "/${utx.bitcoinAddressRecord.isHidden ? "1" : "0"}" + "/${utx.bitcoinAddressRecord.index}"; + publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath); + + utxos.add( + UtxoWithAddress( + utxo: BitcoinUtxo( + txHash: utx.hash, + value: BigInt.from(utx.value), + vout: utx.vout, + scriptType: _getScriptType(address), + isSilentPayment: isSilentPayment, + ), + ownerDetails: UtxoAddressDetails( + publicKey: pubKeyHex, + address: address, + ), + ), + ); + + // sendAll continues for all inputs + if (!sendAll) { + bool amountIsAcquired = leftAmount <= 0; + if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) { + break; + } + } + } + + if (utxos.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + return UtxoDetails( + availableInputs: availableInputs, + unconfirmedCoins: unconfirmedCoins, + utxos: utxos, + vinOutpoints: vinOutpoints, + inputPrivKeyInfos: inputPrivKeyInfos, + publicKeys: publicKeys, + allInputsAmount: allInputsAmount, + spendsSilentPayment: spendsSilentPayment, + spendsUnconfirmedTX: spendsUnconfirmedTX, + ); + } + + Future estimateSendAllTx( + List outputs, + int feeRate, { + String? memo, + bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) async { + final utxoDetails = _createUTXOS( + sendAll: true, + paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + + int fee = await calcFee( + utxos: utxoDetails.utxos, + outputs: outputs, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); + + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } + + // Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change + int amount = utxoDetails.allInputsAmount - fee; + + if (amount <= 0) { + throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee); + } + + // Attempting to send less than the dust limit + if (_isBelowDust(amount)) { + throw BitcoinTransactionNoDustException(); + } + + if (outputs.length == 1) { + outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount)); + } + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + isSendAll: true, + hasChange: false, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } + + int get _dustAmount => 546; + + bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet; + + Future estimateTxForAmount( + int credentialsAmount, + List outputs, + List updatedOutputs, + int feeRate, { + int? inputsCount, + String? memo, + bool? useUnconfirmed, + bool hasSilentPayment = false, + UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any, + }) async { + // Attempting to send less than the dust limit + if (_isBelowDust(credentialsAmount)) { + throw BitcoinTransactionNoDustException(); + } + + final utxoDetails = _createUTXOS( + sendAll: false, + credentialsAmount: credentialsAmount, + inputsCount: inputsCount, + paysToSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + + final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length; + final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX && + utxoDetails.utxos.length == + utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length; + + // How much is being spent - how much is being sent + int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount; + + if (amountLeftForChangeAndFee <= 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + updatedOutputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } + + throw BitcoinTransactionWrongBalanceException(); + } + + final changeAddress = await walletAddresses.getChangeAddress( + inputs: utxoDetails.availableInputs, + outputs: updatedOutputs, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + final address = RegexUtils.addressTypeFromStr(changeAddress.address, network); + updatedOutputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(amountLeftForChangeAndFee), + isChange: true, + )); + + // Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets + final changeDerivationPath = + "${_hardenedDerivationPath(walletInfo.derivationInfo?.derivationPath ?? "m/0'")}" + "/${changeAddress.isHidden ? "1" : "0"}" + "/${changeAddress.index}"; + utxoDetails.publicKeys[address.pubKeyHash()] = + PublicKeyWithDerivationPath('', changeDerivationPath); + + // calcFee updates the silent payment outputs to calculate the tx size accounting + // for taproot addresses, but if more inputs are needed to make up for fees, + // the silent payment outputs need to be recalculated for the new inputs + var temp = outputs.map((output) => output).toList(); + int fee = await calcFee( + utxos: utxoDetails.utxos, + // Always take only not updated bitcoin outputs here so for every estimation + // the SP outputs are re-generated to the proper taproot addresses + outputs: temp, + network: network, + memo: memo, + feeRate: feeRate, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + vinOutpoints: utxoDetails.vinOutpoints, + ); + + updatedOutputs.clear(); + updatedOutputs.addAll(temp); + + if (fee == 0) { + throw BitcoinTransactionNoFeeException(); + } + + int amount = credentialsAmount; + final lastOutput = updatedOutputs.last; + final amountLeftForChange = amountLeftForChangeAndFee - fee; + + if (_isBelowDust(amountLeftForChange)) { + // If has change that is lower than dust, will end up with tx rejected by network rules + // so remove the change amount + updatedOutputs.removeLast(); + outputs.removeLast(); + + if (amountLeftForChange < 0) { + if (!spendingAllCoins) { + return estimateTxForAmount( + credentialsAmount, + outputs, + updatedOutputs, + feeRate, + inputsCount: utxoDetails.utxos.length + 1, + memo: memo, + useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + throw BitcoinTransactionWrongBalanceException(); + } + } + + // if the amount left for change is less than dust, but not less than 0 + // then add it to the fees + fee += amountLeftForChange; + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: false, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } else { + // Here, lastOutput already is change, return the amount left without the fee to the user's address. + updatedOutputs[updatedOutputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); + outputs[outputs.length - 1] = BitcoinOutput( + address: lastOutput.address, + value: BigInt.from(amountLeftForChange), + isSilentPayment: lastOutput.isSilentPayment, + isChange: true, + ); + + return EstimatedTxResult( + utxos: utxoDetails.utxos, + inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos, + publicKeys: utxoDetails.publicKeys, + fee: fee, + amount: amount, + hasChange: true, + isSendAll: spendingAllCoins, + memo: memo, + spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX, + spendsSilentPayment: utxoDetails.spendsSilentPayment, + ); + } + } + + Future createTransactionSuper(Object credentials) async { + try { + // start by updating unspent coins + await updateAllUnspents(); + + final outputs = []; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final hasMultiDestination = transactionCredentials.outputs.length > 1; + final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll; + final memo = transactionCredentials.outputs.first.memo; + final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom; + + int credentialsAmount = 0; + bool hasSilentPayment = false; + + for (final out in transactionCredentials.outputs) { + final outputAmount = out.formattedCryptoAmount!; + + if (!sendAll && _isBelowDust(outputAmount)) { + throw BitcoinTransactionNoDustException(); + } + + if (hasMultiDestination) { + if (out.sendAll) { + throw BitcoinTransactionWrongBalanceException(); + } + } + + credentialsAmount += outputAmount; + + final address = RegexUtils.addressTypeFromStr( + out.isParsedAddress ? out.extractedAddress! : out.address, network); + final isSilentPayment = address is SilentPaymentAddress; + + if (isSilentPayment) { + hasSilentPayment = true; + } + + if (sendAll) { + // The value will be changed after estimating the Tx size and deducting the fee from the total to be sent + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(0), + isSilentPayment: isSilentPayment, + )); + } else { + outputs.add(BitcoinOutput( + address: address, + value: BigInt.from(outputAmount), + isSilentPayment: isSilentPayment, + )); + } + } + + final feeRateInt = transactionCredentials.feeRate != null + ? transactionCredentials.feeRate! + : feeRate(transactionCredentials.priority!); + + EstimatedTxResult estimatedTx; + final updatedOutputs = outputs + .map((e) => BitcoinOutput( + address: e.address, + value: e.value, + isSilentPayment: e.isSilentPayment, + isChange: e.isChange, + )) + .toList(); + + if (sendAll) { + estimatedTx = await estimateSendAllTx( + updatedOutputs, + feeRateInt, + memo: memo, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } else { + estimatedTx = await estimateTxForAmount( + credentialsAmount, + outputs, + updatedOutputs, + feeRateInt, + memo: memo, + hasSilentPayment: hasSilentPayment, + coinTypeToSpendFrom: coinTypeToSpendFrom, + ); + } + + if (walletInfo.isHardwareWallet) { + final transaction = await buildHardwareWalletTransaction( + utxos: estimatedTx.utxos, + outputs: updatedOutputs, + publicKeys: estimatedTx.publicKeys, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: true, + ); + + return PendingBitcoinTransaction( + transaction, + type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot + )..addListener((transaction) async { + transactionHistory.addOne(transaction); + await updateBalance(); + await updateAllUnspents(); + }); + } + + BasedBitcoinTransacationBuilder txb = BitcoinTransactionBuilder( + utxos: estimatedTx.utxos, + outputs: updatedOutputs, + fee: BigInt.from(estimatedTx.fee), + network: network, + memo: estimatedTx.memo, + outputOrdering: BitcoinOrdering.none, + enableRBF: !estimatedTx.spendsUnconfirmedTX, + ); + + bool hasTaprootInputs = false; + + final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) { + String error = "Cannot find private key."; + + ECPrivateInfo? key; + + if (estimatedTx.inputPrivKeyInfos.isEmpty) { + error += "\nNo private keys generated."; + } else { + error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}"; + + key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) { + final elemPubkey = element.privkey.getPublic().toHex(); + if (elemPubkey == publicKey) { + return true; + } else { + error += "\nExpected: $publicKey"; + error += "\nPubkey: $elemPubkey"; + return false; + } + }); + } + + if (key == null) { + throw Exception(error); + } + + if (utxo.utxo.isP2tr()) { + hasTaprootInputs = true; + return key.privkey.signTapRoot( + txDigest, + sighash: sighash, + tweak: utxo.utxo.isSilentPayment != true, + ); + } else { + return key.privkey.signInput(txDigest, sigHash: sighash); + } + }); + + return PendingBitcoinTransaction(transaction, type, + electrumClient: electrumClient, + amount: estimatedTx.amount, + fee: estimatedTx.fee, + feeRate: feeRateInt.toString(), + network: network, + hasChange: estimatedTx.hasChange, + isSendAll: estimatedTx.isSendAll, + hasTaprootInputs: hasTaprootInputs, + utxos: estimatedTx.utxos, + publicKeys: estimatedTx.publicKeys) + ..addListener((transaction) async { + transactionHistory.addOne(transaction); + if (estimatedTx.spendsSilentPayment) { + transactionHistory.transactions.values.forEach((tx) { + tx.unspents?.removeWhere( + (unspent) => estimatedTx.utxos.any((e) => e.utxo.txHash == unspent.hash)); + transactionHistory.addOne(tx); + }); + } + + unspentCoins + .removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash)); + + await updateBalance(); + await updateAllUnspents(); + }); + } catch (e) { + throw e; + } + } + + String toJSON() => json.encode({ + 'mnemonic': super.seed, + 'xpub': xpub, + 'passphrase': passphrase ?? '', + 'account_index': walletAddresses.currentReceiveAddressIndexByType, + 'change_address_index': walletAddresses.currentChangeAddressIndexByType, + 'addresses': walletAddresses.allAddresses.map((addr) => addr.toJSON()).toList(), + 'address_page_type': walletInfo.addressPageType == null + ? SegwitAddresType.p2wpkh.toString() + : walletInfo.addressPageType.toString(), + 'balance': balance[currency]?.toJSON(), + 'derivationTypeIndex': walletInfo.derivationInfo?.derivationType?.index, + 'derivationPath': walletInfo.derivationInfo?.derivationPath, + 'silent_addresses': walletAddresses.silentAddresses.map((addr) => addr.toJSON()).toList(), + 'silent_address_index': walletAddresses.currentSilentAddressIndex.toString(), + 'mweb_addresses': walletAddresses.mwebAddresses.map((addr) => addr.toJSON()).toList(), + 'alwaysScan': alwaysScan, + }); + + @action + Future?> fetchUnspent(covariant LitecoinAddressRecord address) async { + List updatedUnspentCoins = []; + + final unspents = await electrumClient.getListUnspent(address.getScriptHash(network)); + + // Failed to fetch unspents + if (unspents == null) return null; + + await Future.wait(unspents.map((unspent) async { + try { + final coin = BitcoinUnspent.fromJSON(address, unspent); + final tx = await fetchTransactionInfo(hash: coin.hash); + coin.isChange = address.isHidden; + coin.confirmations = tx?.confirmations; + + updatedUnspentCoins.add(coin); + } catch (_) {} + })); + + return updatedUnspentCoins; + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = + unspentCoinsInfo.values.where((record) => record.walletId == id); + + for (final element in currentWalletUnspentCoins) { + if (RegexUtils.addressTypeFromStr(element.address, network) is MwebAddress) continue; + + final existUnspentCoins = unspentCoins.where((coin) => element == coin); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + printV("refreshUnspentCoinsInfo $e"); + } + } + + int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize(); + + Future getTransactionExpanded( + {required String hash, int? height}) async { + String transactionHex; + int? time; + int? confirmations; + + final verboseTransaction = await electrumClient.getTransactionVerbose(hash: hash); + + if (verboseTransaction.isEmpty) { + transactionHex = await electrumClient.getTransactionHex(hash: hash); + + if (height != null && height > 0 && await checkIfMempoolAPIIsEnabled()) { + try { + final blockHash = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block-height/$height")) + .timeout(Duration(seconds: 15)); + + if (blockHash.statusCode == 200 && + blockHash.body.isNotEmpty && + jsonDecode(blockHash.body) != null) { + final blockResponse = await ProxyWrapper() + .get(clearnetUri: Uri.parse("https://mempool.cakewallet.com/api/v1/block/${blockHash}")) + .timeout(Duration(seconds: 15)); + + if (blockResponse.statusCode == 200 && + blockResponse.body.isNotEmpty && + jsonDecode(blockResponse.body)['timestamp'] != null) { + time = int.parse(jsonDecode(blockResponse.body)['timestamp'].toString()); + } + } + } catch (_) {} + } + } else { + transactionHex = verboseTransaction['hex'] as String; + time = verboseTransaction['time'] as int?; + confirmations = verboseTransaction['confirmations'] as int?; + } + + if (height != null) { + if (time == null && height > 0) { + time = (getDateByBitcoinHeight(height).millisecondsSinceEpoch / 1000).round(); + } + + if (confirmations == null) { + final tip = await getUpdatedChainTip(); + if (tip > 0 && height > 0) { + // Add one because the block itself is the first confirmation + confirmations = tip - height + 1; + } + } + } + + final original = BtcTransaction.fromRaw(transactionHex); + final ins = []; + + for (final vin in original.inputs) { + final verboseTransaction = await electrumClient.getTransactionVerbose(hash: vin.txId); + + final String inputTransactionHex; + + if (verboseTransaction.isEmpty) { + inputTransactionHex = await electrumClient.getTransactionHex(hash: hash); + } else { + inputTransactionHex = verboseTransaction['hex'] as String; + } + + ins.add(BtcTransaction.fromRaw(inputTransactionHex)); + } + + return ElectrumTransactionBundle( + original, + ins: ins, + time: time, + confirmations: confirmations ?? 0, + ); + } + + Future fetchTransactionInfo( + {required String hash, int? height, bool? retryOnFailure}) async { + try { + return ElectrumTransactionInfo.fromElectrumBundle( + await getTransactionExpanded(hash: hash, height: height), + walletInfo.type, + network, + addresses: addressesSet, + height: height, + ); + } catch (e) { + if (e is FormatException && retryOnFailure == true) { + await Future.delayed(const Duration(seconds: 2)); + return fetchTransactionInfo(hash: hash, height: height); + } + return null; + } + } + + @override + Future> fetchTransactions() async { + try { + final Map historiesWithDetails = {}; + if (type == WalletType.litecoin) { + await Future.wait(LITECOIN_ADDRESS_TYPES + .where((type) => type != SegwitAddresType.mweb) + .map((type) => fetchTransactionsForAddressType(historiesWithDetails, type))); + } + + transactionHistory.transactions.values.forEach((tx) async { + final isPendingSilentPaymentUtxo = + (tx.isPending || tx.confirmations == 0) && historiesWithDetails[tx.id] == null; + + if (isPendingSilentPaymentUtxo) { + final info = + await fetchTransactionInfo(hash: tx.id, height: tx.height, retryOnFailure: true); + + if (info != null) { + tx.confirmations = info.confirmations; + tx.isPending = tx.confirmations == 0; + transactionHistory.addOne(tx); + await transactionHistory.save(); + } + } + }); + + return historiesWithDetails; + } catch (e) { + printV("fetchTransactions $e"); + return {}; + } + } + + Future> _fetchAddressHistory( + BitcoinAddressRecord addressRecord, int? currentHeight) async { + String txid = ""; + + try { + final Map historiesWithDetails = {}; + + final history = await electrumClient.getHistory(addressRecord.getScriptHash(network)); + + if (history.isNotEmpty) { + addressRecord.setAsUsed(); + + await Future.wait(history.map((transaction) async { + txid = transaction['tx_hash'] as String; + final height = transaction['height'] as int; + final storedTx = transactionHistory.transactions[txid]; + + if (storedTx != null) { + if (height > 0) { + storedTx.height = height; + // the tx's block itself is the first confirmation so add 1 + if ((currentHeight ?? 0) > 0) { + storedTx.confirmations = currentHeight! - height + 1; + } + storedTx.isPending = storedTx.confirmations == 0; + } + + historiesWithDetails[txid] = storedTx; + } else { + final tx = await fetchTransactionInfo(hash: txid, height: height, retryOnFailure: true); + + if (tx != null) { + historiesWithDetails[txid] = tx; + + // Got a new transaction fetched, add it to the transaction history + // instead of waiting all to finish, and next time it will be faster + + if (this is LitecoinWallet) { + // if we have a peg out transaction with the same value + // that matches this received transaction, mark it as being from a peg out: + for (final tx2 in transactionHistory.transactions.values) { + final heightDiff = ((tx2.height ?? 0) - (tx.height ?? 0)).abs(); + // this isn't a perfect matching algorithm since we don't have the right input/output information from these transaction models (the addresses are in different formats), but this should be more than good enough for now as it's extremely unlikely a user receives the EXACT same amount from 2 different sources and one of them is a peg out and the other isn't WITHIN 5 blocks of each other + if (tx2.additionalInfo["isPegOut"] == true && + tx2.amount == tx.amount && + heightDiff <= 5) { + tx.additionalInfo["fromPegOut"] = true; + } + } + } + transactionHistory.addOne(tx); + await transactionHistory.save(); + } + } + + return Future.value(null); + })); + } + + return historiesWithDetails; + } catch (e) { + // _onError?.call(FlutterErrorDetails( + // exception: "$txid - $e", + // stack: stacktrace, + // library: this.runtimeType.toString(), + // )); + return {}; + } + } + + + Future fetchTransactionsForAddressType( + Map historiesWithDetails, + BitcoinAddressType type, + ) async { + final addressesByType = walletAddresses.allAddresses.where((addr) => addr.type == type); + final hiddenAddresses = addressesByType.where((addr) => addr.isHidden == true); + final receiveAddresses = addressesByType.where((addr) => addr.isHidden == false); + walletAddresses.hiddenAddresses.addAll(hiddenAddresses.map((e) => e.address)); + await walletAddresses.saveAddressesInBox(); + await Future.wait(addressesByType.map((addressRecord) async { + final history = await _fetchAddressHistory(addressRecord, await getCurrentChainTip()); + + if (history.isNotEmpty) { + addressRecord.txCount = history.length; + historiesWithDetails.addAll(history); + + final matchedAddresses = addressRecord.isHidden ? hiddenAddresses : receiveAddresses; + final isUsedAddressUnderGap = matchedAddresses.toList().indexOf(addressRecord) >= + matchedAddresses.length - + (addressRecord.isHidden + ? LitecoinWalletAddressesBase.defaultChangeAddressesCount + : LitecoinWalletAddressesBase.defaultReceiveAddressesCount); + + if (isUsedAddressUnderGap) { + final prevLength = walletAddresses.allAddresses.length; + + // Discover new addresses for the same address type until the gap limit is respected + await walletAddresses.discoverAddresses( + matchedAddresses.toList(), + addressRecord.isHidden, + (address) async { + await subscribeForUpdates(); + return _fetchAddressHistory(address, await getCurrentChainTip()) + .then((history) => history.isNotEmpty ? address.address : null); + }, + type: type, + ); + + final newLength = walletAddresses.allAddresses.length; + + if (newLength > prevLength) { + await fetchTransactionsForAddressType(historiesWithDetails, type); + } + } + } + })); + } + + static String _hardenedDerivationPath(String derivationPath) => + derivationPath.substring(0, derivationPath.lastIndexOf("'") + 1); @observable SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); @@ -169,8 +1071,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { required EncryptionFileUtils encryptionFileUtils, String? passphrase, String? addressPageType, - List? initialAddresses, - List? initialMwebAddresses, + List? initialAddresses, + List? initialMwebAddresses, ElectrumBalance? initialBalance, Map? initialRegularAddressIndex, Map? initialChangeAddressIndex}) async { @@ -274,8 +1176,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { password: password, walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfo, - initialAddresses: snp?.addresses, - initialMwebAddresses: snp?.mwebAddresses, + initialAddresses: snp?.addresses as List?, + initialMwebAddresses: snp?.mwebAddresses as List?, initialBalance: snp?.balance, seedBytes: seedBytes, passphrase: passphrase, @@ -1060,7 +1962,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @override Future createTransaction(Object credentials) async { try { - var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction; + var tx = await createTransactionSuper(credentials) as PendingBitcoinTransaction; tx.isMweb = mwebEnabled; if (!mwebEnabled) { @@ -1425,4 +2327,27 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { return BtcTransaction.fromRaw(rawHex); } + + BitcoinAddressType _getScriptType(BitcoinBaseAddress type) { + if (type is P2pkhAddress) { + return P2pkhAddressType.p2pkh; + } else if (type is P2shAddress) { + return P2shAddressType.p2wpkhInP2sh; + } else if (type is P2wshAddress) { + return SegwitAddresType.p2wsh; + } else if (type is P2trAddress) { + return SegwitAddresType.p2tr; + } else if (type is MwebAddress) { + return SegwitAddresType.mweb; + } else if (type is SilentPaymentsAddresType) { + return SilentPaymentsAddresType.p2sp; + } else { + return SegwitAddresType.p2wpkh; + } + } + + static const List LITECOIN_ADDRESS_TYPES = [ + SegwitAddresType.p2wpkh, + SegwitAddresType.mweb, + ]; } diff --git a/cw_bitcoin/lib/litecoin_wallet_addresses.dart b/cw_bitcoin/lib/litecoin_wallet_addresses.dart index bbb987766..e6c1e0e43 100644 --- a/cw_bitcoin/lib/litecoin_wallet_addresses.dart +++ b/cw_bitcoin/lib/litecoin_wallet_addresses.dart @@ -2,16 +2,16 @@ import 'dart:async'; import 'dart:io' show Platform; import 'dart:typed_data'; -import 'package:bitcoin_base/bitcoin_base.dart'; +import 'package:bitcoin_base_old/bitcoin_base.dart'; import 'package:blockchain_utils/blockchain_utils.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_unspent.dart'; -import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/utils.dart'; import 'package:cw_bitcoin/electrum_wallet_addresses.dart'; import 'package:cw_core/unspent_coin_type.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:cw_mweb/cw_mweb.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; @@ -33,7 +33,10 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with super.initialMwebAddresses, super.initialRegularAddressIndex, super.initialChangeAddressIndex, - }) : super(walletInfo) { + }) : _addressPageType = + (walletInfo.addressPageType != null + ? BitcoinAddressType.fromValue(walletInfo.addressPageType!) + : SegwitAddresType.p2wpkh), super(walletInfo) { for (int i = 0; i < mwebAddresses.length; i++) { mwebAddrs.add(mwebAddresses[i].address); } @@ -210,4 +213,403 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with .where((element) => element.type == SegwitAddresType.p2wpkh && !element.isUsed); return addresses.first.address; } + + static const defaultReceiveAddressesCount = 22; + static const defaultChangeAddressesCount = 17; + static const gap = 20; + + @observable + late BitcoinAddressType _addressPageType; + + @computed + BitcoinAddressType get addressPageType => _addressPageType; + + + @override + @computed + String get address { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + if (activeSilentAddress != null) { + return activeSilentAddress!; + } + + return silentAddress.toString(); + } + + final typeMatchingAddresses = super.allAddresses.where((addr) => !addr.isHidden && _isAddressPageTypeMatch(addr)).toList(); + final typeMatchingReceiveAddresses = typeMatchingAddresses.where((addr) => !addr.isUsed).toList(); + + if (!isEnabledAutoGenerateSubaddress) { + if (previousAddressRecord != null && + previousAddressRecord!.type == addressPageType) { + return previousAddressRecord!.address; + } + + if (typeMatchingAddresses.isNotEmpty) { + return typeMatchingAddresses.first.address; + } + + return generateNewAddress().address; + } + + if (typeMatchingAddresses.isEmpty || typeMatchingReceiveAddresses.isEmpty) { + return generateNewAddress().address; + } + + final prev = previousAddressRecord; + if (prev != null && prev.type == addressPageType && !prev.isUsed) { + return prev.address; + } + + return typeMatchingReceiveAddresses.first.address; + } + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + set address(String addr) { + if (addr == "Silent Payments" && SilentPaymentsAddresType.p2sp != addressPageType) { + return; + } + if (addressPageType == SilentPaymentsAddresType.p2sp) { + final selected = silentAddresses.firstWhere((addressRecord) => addressRecord.address == addr); + + if (selected.silentPaymentTweak != null && silentAddress != null) { + activeSilentAddress = + silentAddress!.toLabeledSilentPaymentAddress(selected.index).toString(); + } else { + activeSilentAddress = silentAddress!.toString(); + } + return; + } + try { + final addressRecord = super.allAddresses.firstWhere( + (addressRecord) => addressRecord.address == addr, + ); + + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); + } catch (e) { + printV("ElectrumWalletAddressBase: set address ($addr): $e"); + } + } + + @override + String get primaryAddress => getAddress(index: 0, hd: mainHd, addressType: addressPageType); + + int get currentReceiveAddressIndex => + currentReceiveAddressIndexByType[_addressPageType.toString()] ?? 0; + + void set currentReceiveAddressIndex(int index) => + currentReceiveAddressIndexByType[_addressPageType.toString()] = index; + + int get currentChangeAddressIndex => + currentChangeAddressIndexByType[_addressPageType.toString()] ?? 0; + + void set currentChangeAddressIndex(int index) => + currentChangeAddressIndexByType[_addressPageType.toString()] = index; + + @observable + BitcoinAddressRecord? previousAddressRecord; + + @computed + int get totalCountOfReceiveAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { + if (!addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); + + @computed + int get totalCountOfChangeAddresses => addressesByReceiveType.fold(0, (acc, addressRecord) { + if (addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); + + Map get labels { + final G = ECPublic.fromBytes(BigintUtils.toBytes(Curves.generatorSecp256k1.x, length: 32)); + final labels = {}; + for (int i = 0; i < silentAddresses.length; i++) { + final silentAddressRecord = silentAddresses[i]; + final silentPaymentTweak = silentAddressRecord.silentPaymentTweak; + + if (silentPaymentTweak != null && + SilentPaymentAddress.regex.hasMatch(silentAddressRecord.address)) { + labels[G + .tweakMul(BigintUtils.fromBytes(BytesUtils.fromHexString(silentPaymentTweak))) + .toHex()] = silentPaymentTweak; + } + } + return labels; + } + + @action + BaseBitcoinAddressRecord generateNewAddress({String label = ''}) { + final newAddressIndex = addressesByReceiveType.fold( + 0, (int acc, addressRecord) => addressRecord.isHidden == false ? acc + 1 : acc); + + final address = BitcoinAddressRecord( + getAddress(index: newAddressIndex, hd: mainHd, addressType: addressPageType), + index: newAddressIndex, + isHidden: false, + name: label, + type: addressPageType, + network: network, + ); + Future.delayed(Duration.zero, () { + super.allAddresses.add(address); + updateAddressesByMatch(); + }); + return address; + } + + void addLitecoinAddressTypes() { + final lastP2wpkh = super.allAddresses + .where((addressRecord) => + _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.p2wpkh)) + .toList() + .last; + if (lastP2wpkh.address != address) { + addressesMap[lastP2wpkh.address] = 'P2WPKH'; + } else { + addressesMap[address] = 'Active - P2WPKH'; + } + + final lastMweb = super.allAddresses.firstWhere( + (addressRecord) => _isUnusedReceiveAddressByType(addressRecord, SegwitAddresType.mweb)); + if (lastMweb.address != address) { + addressesMap[lastMweb.address] = 'MWEB'; + } else { + addressesMap[address] = 'Active - MWEB'; + } + } + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = 'Active'; + + allAddressesMap.clear(); + super.allAddresses.forEach((addressRecord) { + allAddressesMap[addressRecord.address] = addressRecord.name; + }); + + switch (walletInfo.type) { + case WalletType.bitcoin: + addBitcoinAddressTypes(); + break; + case WalletType.litecoin: + addLitecoinAddressTypes(); + break; + case WalletType.bitcoinCash: + addBitcoinCashAddressTypes(); + break; + default: + break; + } + + await saveAddressesInBox(); + } catch (e) { + printV("updateAddresses $e"); + } + } + + @action + void updateAddress(String address, String label) { + BaseBitcoinAddressRecord? foundAddress; + super.allAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); + silentAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); + mwebAddresses.forEach((addressRecord) { + if (addressRecord.address == address) { + foundAddress = addressRecord; + } + }); + + if (foundAddress != null) { + foundAddress!.setNewName(label); + + if (foundAddress is BitcoinAddressRecord) { + final index = super.allAddresses.indexOf(foundAddress as BitcoinAddressRecord); + super.allAddresses.remove(foundAddress); + super.allAddresses.insert(index, foundAddress as BitcoinAddressRecord); + } else { + final index = silentAddresses.indexOf(foundAddress as BitcoinSilentPaymentAddressRecord); + silentAddresses.remove(foundAddress); + silentAddresses.insert(index, foundAddress as BitcoinSilentPaymentAddressRecord); + } + } + } + + @action + void updateAddressesByMatch() { + if (addressPageType == SilentPaymentsAddresType.p2sp) { + addressesByReceiveType.clear(); + addressesByReceiveType.addAll(silentAddresses); + return; + } + + addressesByReceiveType.clear(); + addressesByReceiveType.addAll(super.allAddresses.where(_isAddressPageTypeMatch).toList()); + } + + @action + void updateReceiveAddresses() { + receiveAddresses.removeRange(0, receiveAddresses.length); + final newAddresses = + super.allAddresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + receiveAddresses.addAll(newAddresses); + } + + @action + void updateChangeAddresses() { + changeAddresses.removeRange(0, changeAddresses.length); + final newAddresses = super.allAddresses.where((addressRecord) => + addressRecord.isHidden && + !addressRecord.isUsed && + // TODO: feature to change change address type. For now fixed to p2wpkh, the cheapest type + (walletInfo.type != WalletType.bitcoin || addressRecord.type == SegwitAddresType.p2wpkh)); + changeAddresses.addAll(newAddresses); + } + + @action + Future discoverAddresses(List addressList, bool isHidden, + Future Function(BitcoinAddressRecord) getAddressHistory, + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { + final newAddresses = await _createNewAddresses(gap, + startIndex: addressList.length, isHidden: isHidden, type: type); + addAddresses(newAddresses); + + final addressesWithHistory = await Future.wait(newAddresses.map(getAddressHistory)); + final isLastAddressUsed = addressesWithHistory.last == addressList.last.address; + + if (isLastAddressUsed) { + discoverAddresses(addressList, isHidden, getAddressHistory, type: type); + } + } + + Future _generateInitialAddresses( + {BitcoinAddressType type = SegwitAddresType.p2wpkh}) async { + var countOfReceiveAddresses = 0; + var countOfHiddenAddresses = 0; + + super.allAddresses.forEach((addr) { + if (addr.type == type) { + if (addr.isHidden) { + countOfHiddenAddresses += 1; + return; + } + + countOfReceiveAddresses += 1; + } + }); + + if (countOfReceiveAddresses < defaultReceiveAddressesCount) { + final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfReceiveAddresses, isHidden: false, type: type); + addAddresses(newAddresses); + } + + if (countOfHiddenAddresses < defaultChangeAddressesCount) { + final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfHiddenAddresses, isHidden: true, type: type); + addAddresses(newAddresses); + } + } + + Future> _createNewAddresses(int count, + {int startIndex = 0, bool isHidden = false, BitcoinAddressType? type}) async { + final list = []; + + for (var i = startIndex; i < count + startIndex; i++) { + final address = BitcoinAddressRecord( + await getAddressAsync(index: i, hd: _getHd(isHidden), addressType: type ?? addressPageType), + index: i, + isHidden: isHidden, + type: type ?? addressPageType, + network: network, + ); + list.add(address); + } + + return list; + } + + @action + void addAddresses(Iterable addresses) { + final addressesSet = super.allAddresses.toSet(); + addressesSet.addAll(addresses); + super.allAddresses.clear(); + super.allAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + + @action + void addSilentAddresses(Iterable addresses) { + final addressesSet = this.silentAddresses.toSet(); + addressesSet.addAll(addresses); + this.silentAddresses.clear(); + this.silentAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + + @action + void addMwebAddresses(Iterable addresses) { + final addressesSet = this.mwebAddresses.toSet(); + addressesSet.addAll(addresses); + this.mwebAddresses.clear(); + this.mwebAddresses.addAll(addressesSet); + updateAddressesByMatch(); + } + + void _validateAddresses() { + super.allAddresses.forEach((element) async { + if (element.type == SegwitAddresType.mweb) { + // this would add a ton of startup lag for mweb addresses since we have 1000 of them + return; + } + if (!element.isHidden && + element.address != + await getAddressAsync(index: element.index, hd: mainHd, addressType: element.type)) { + element.isHidden = true; + } else if (element.isHidden && + element.address != + await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) { + element.isHidden = false; + } + }); + } + + @action + Future setAddressType(BitcoinAddressType type) async { + _addressPageType = type; + updateAddressesByMatch(); + walletInfo.addressPageType = addressPageType.toString(); + await walletInfo.save(); + } + + bool _isAddressPageTypeMatch(BitcoinAddressRecord addressRecord) { + return _isAddressByType(addressRecord, addressPageType); + } + + Bip32Slip10Secp256k1 _getHd(bool isHidden) => isHidden ? sideHd : mainHd; + + bool _isAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => addr.type == type; + + bool _isUnusedReceiveAddressByType(BitcoinAddressRecord addr, BitcoinAddressType type) => + !addr.isHidden && !addr.isUsed && addr.type == type; } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c2987894c..6483cf0f8 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -81,11 +81,20 @@ packages: source: git version: "1.0.1" bitcoin_base: + dependency: transitive + description: + path: "." + ref: cake-update-v7 + resolved-ref: f577e83fe78766b2655ea0602baa9299b953a31b + url: "https://github.com/cake-tech/bitcoin_base" + source: git + version: "4.7.0" + bitcoin_base_old: dependency: "direct overridden" description: path: "." - ref: cake-update-v9 - resolved-ref: bb4318511312a454fd91bf49042e25ecc855e4ac + ref: cake-update-old + resolved-ref: f39aa0b62642eaf1e9de5b31e2395148e73c7fd2 url: "https://github.com/cake-tech/bitcoin_base" source: git version: "4.7.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index c24732c3a..035586fe8 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -70,10 +70,10 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 protobuf: ^3.1.0 - bitcoin_base: + bitcoin_base_old: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v9 + ref: cake-update-old pointycastle: 3.7.4 ffi: 2.1.0 diff --git a/cw_bitcoin_cash/pubspec.yaml b/cw_bitcoin_cash/pubspec.yaml index e78261f9a..744d7ed56 100644 --- a/cw_bitcoin_cash/pubspec.yaml +++ b/cw_bitcoin_cash/pubspec.yaml @@ -39,10 +39,10 @@ dev_dependencies: dependency_overrides: watcher: ^1.1.0 - bitcoin_base: + bitcoin_base_old: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v9 + ref: cake-update-old # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 575b43d5f..bc2bf0fb6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,10 +2,6 @@ PODS: - connectivity_plus (0.0.1): - Flutter - CryptoSwift (1.8.4) - - cw_decred (0.0.1): - - Flutter - - cw_mweb (0.0.1): - - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -76,7 +72,6 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - payjoin_flutter (0.20.0) - permission_handler_apple (9.3.0): - Flutter - reown_yttrium (0.0.1): @@ -92,8 +87,6 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sp_scanner (0.0.1): - - Flutter - SwiftyGif (5.4.5) - uni_links (0.0.1): - Flutter @@ -109,8 +102,6 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift - - cw_decred (from `.symlinks/plugins/cw_decred/ios`) - - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -127,13 +118,11 @@ DEPENDENCIES: - integration_test (from `.symlinks/plugins/integration_test/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - payjoin_flutter (from `.symlinks/plugins/payjoin_flutter/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - reown_yttrium (from `.symlinks/plugins/reown_yttrium/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sp_scanner (from `.symlinks/plugins/sp_scanner/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -152,10 +141,6 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" - cw_decred: - :path: ".symlinks/plugins/cw_decred/ios" - cw_mweb: - :path: ".symlinks/plugins/cw_mweb/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -188,8 +173,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" - payjoin_flutter: - :path: ".symlinks/plugins/payjoin_flutter/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" reown_yttrium: @@ -200,8 +183,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sp_scanner: - :path: ".symlinks/plugins/sp_scanner/ios" uni_links: :path: ".symlinks/plugins/uni_links/ios" universal_ble: @@ -214,8 +195,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d CryptoSwift: e64e11850ede528a02a0f3e768cec8e9d92ecb90 - cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a - cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 @@ -235,14 +214,12 @@ SPEC CHECKSUMS: OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - payjoin_flutter: 6397d7b698cdad6453be4949ab6aca1863f6c5e5 permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 reown_yttrium: c0e87e5965fa60a3559564cc35cffbba22976089 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sp_scanner: eaa617fa827396b967116b7f1f43549ca62e9a12 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 uni_links: d97da20c7701486ba192624d99bffaaffcfc298a universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 794305883..674a5b64b 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -511,7 +511,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.monero; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -660,7 +660,7 @@ ); MARKETING_VERSION = 1.0.1; ONLY_ACTIVE_ARCH = NO; - PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.monero; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -700,7 +700,7 @@ "$(PROJECT_DIR)/Flutter", ); MARKETING_VERSION = 1.0.1; - PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; + PRODUCT_BUNDLE_IDENTIFIER = com.cakewallet.monero; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 9f7f17ae4..7d67d8ce1 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -161,10 +161,10 @@ dependency_overrides: ref: cake flutter_secure_storage_platform_interface: 1.0.2 protobuf: ^3.1.0 - bitcoin_base: + bitcoin_base_old: git: url: https://github.com/cake-tech/bitcoin_base - ref: cake-update-v9 + ref: cake-update-old ffi: 2.1.0 ledger_flutter_plus: git: