diff --git a/lib/wallets/wallet/impl/namecoin_wallet.dart b/lib/wallets/wallet/impl/namecoin_wallet.dart index 64dc23801..bf6f43e41 100644 --- a/lib/wallets/wallet/impl/namecoin_wallet.dart +++ b/lib/wallets/wallet/impl/namecoin_wallet.dart @@ -41,11 +41,7 @@ String nameSaltKeyBuilder(String txid, String walletId, int txPos) { } String encodeNameSaltData(String name, String salt, String value) => - jsonEncode({ - "name": name, - "salt": salt, - "value": value, - }); + jsonEncode({"name": name, "salt": salt, "value": value}); ({String salt, String name, String value}) decodeNameSaltData(String value) { try { @@ -76,25 +72,26 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> @override Future<List<Address>> fetchAddressesForElectrumXScan() async { - final allAddresses = await mainDB - .getAddresses(walletId) - .filter() - .not() - .group( - (q) => q - .typeEqualTo(AddressType.nonWallet) - .or() - .subTypeEqualTo(AddressSubType.nonWallet), - ) - .findAll(); + final allAddresses = + await mainDB + .getAddresses(walletId) + .filter() + .not() + .group( + (q) => q + .typeEqualTo(AddressType.nonWallet) + .or() + .subTypeEqualTo(AddressSubType.nonWallet), + ) + .findAll(); return allAddresses; } -// =========================================================================== + // =========================================================================== @override Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map<String, dynamic> jsonUTXO, String? scriptPubKeyHex, Map<String, dynamic> jsonTX, @@ -108,9 +105,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> } @override - Future<UTXO> parseUTXO({ - required Map<String, dynamic> jsonUTXO, - }) async { + Future<UTXO> parseUTXO({required Map<String, dynamic> jsonUTXO}) async { final txn = await electrumXCachedClient.getTransaction( txHash: jsonUTXO["tx_hash"] as String, verbose: true, @@ -136,7 +131,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> if (output["n"] == vout) { utxoOwnerAddress = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; + output["scriptPubKey"]?["address"] as String?; // check for nameOp if (output["scriptPubKey"]?["nameOp"] != null) { @@ -145,19 +140,15 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> blockReason = "Contains name"; try { - final rawNameOP = (output["scriptPubKey"]["nameOp"] as Map) - .cast<String, dynamic>(); + final rawNameOP = + (output["scriptPubKey"]["nameOp"] as Map) + .cast<String, dynamic>(); otherDataString = jsonEncode({ UTXOOtherDataKeys.nameOpData: jsonEncode(rawNameOP), }); - final nameOp = OpNameData( - rawNameOP, - jsonUTXO["height"] as int, - ); - Logging.instance.i( - "nameOp:\n$nameOp", - ); + final nameOp = OpNameData(rawNameOP, jsonUTXO["height"] as int); + Logging.instance.i("nameOp:\n$nameOp"); switch (nameOp.op) { case OpName.nameNew: @@ -193,7 +184,8 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> name: label ?? "", isBlocked: shouldBlock, blockedReason: blockReason, - isCoinbase: txn["is_coinbase"] as bool? ?? + isCoinbase: + txn["is_coinbase"] as bool? ?? txn["is-coinbase"] as bool? ?? txn["iscoinbase"] as bool? ?? isCoinbase, @@ -231,30 +223,34 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set<String> receivingAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.receiving) - .map((e) => e.value) - .toSet(); - final Set<String> changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set<String> receivingAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.receiving) + .map((e) => e.value) + .toSet(); + final Set<String> changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List<Map<String, dynamic>> allTxHashes = - await fetchHistory(allAddressesSet); + final List<Map<String, dynamic>> allTxHashes = await fetchHistory( + allAddressesSet, + ); // Only parse new txs (not in db yet). final List<Map<String, dynamic>> allTransactions = []; for (final txHash in allTxHashes) { // Check for duplicates by searching for tx by tx_hash in db. - final storedTx = await mainDB.isar.transactionV2s - .where() - .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) - .findFirst(); + final storedTx = + await mainDB.isar.transactionV2s + .where() + .txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId) + .findFirst(); if (storedTx == null || storedTx.height == null || @@ -267,8 +263,9 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> ); // Only tx to list once. - if (allTransactions - .indexWhere((e) => e["txid"] == tx["txid"] as String) == + if (allTransactions.indexWhere( + (e) => e["txid"] == tx["txid"] as String, + ) == -1) { tx["height"] = txHash["height"]; allTransactions.add(tx); @@ -418,7 +415,8 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -456,10 +454,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> final data = decodeNameSaltData(encoded); if (data.name == name) { - return ( - data: null, - nameState: NameState.unavailable, - ); + return (data: null, nameState: NameState.unavailable); } } } @@ -485,9 +480,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> opNameData = OpNameData.fromTx(txMap, txHeight); final isExpired = opNameData.expired(await chainHeight); - Logging.instance.i( - "Name $opNameData \nis expired = $isExpired", - ); + Logging.instance.i("Name $opNameData \nis expired = $isExpired"); available = isExpired; } catch (_) { available = false; // probably @@ -508,23 +501,22 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> /// Must be called in refresh() AFTER the wallet's UTXOs have been updated! Future<void> checkAutoRegisterNameNewOutputs() async { - Logging.instance.t( - "$walletId checkAutoRegisterNameNewOutputs()", - ); + Logging.instance.t("$walletId checkAutoRegisterNameNewOutputs()"); try { final currentHeight = await chainHeight; // not ideal filtering - final utxos = await mainDB - .getUTXOs(walletId) - .filter() - .otherDataIsNotNull() - .and() - .blockHeightIsNotNull() - .and() - .blockHeightGreaterThan(0) - .and() - .blockHeightLessThan(currentHeight - kNameWaitBlocks) - .findAll(); + final utxos = + await mainDB + .getUTXOs(walletId) + .filter() + .otherDataIsNotNull() + .and() + .blockHeightIsNotNull() + .and() + .blockHeightGreaterThan(0) + .and() + .blockHeightLessThan(currentHeight - kNameWaitBlocks) + .findAll(); Logging.instance.t( "_unknownNameNewOutputs(count=${_unknownNameNewOutputs.length})" @@ -539,9 +531,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> for (final utxo in utxos) { final nameOp = getOpNameDataFrom(utxo); if (nameOp != null) { - Logging.instance.t( - "Found OpName: $nameOp\n\nIN UTXO: $utxo", - ); + Logging.instance.t("Found OpName: $nameOp\n\nIN UTXO: $utxo"); if (nameOp.op == OpName.nameNew) { // at this point we should have an unspent UTXO that is at least @@ -646,14 +636,10 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> switch (txData.opNameState!.type) { case OpName.nameNew: - assert( - nameAmount.raw == BigInt.from(kNameNewAmountSats), - ); + assert(nameAmount.raw == BigInt.from(kNameNewAmountSats)); break; case OpName.nameFirstUpdate || OpName.nameUpdate: - assert( - nameAmount.raw == BigInt.from(kNameAmountSats), - ); + assert(nameAmount.raw == BigInt.from(kNameAmountSats)); break; } } @@ -671,9 +657,10 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> ); // TODO: [prio=high]: check this opt in rbf - final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF - ? 0xffffffff - 10 - : 0xffffffff - 1; + final sequence = + this is RbfInterface && (this as RbfInterface).flagOptInRBF + ? 0xffffffff - 10 + : 0xffffffff - 1; // Add transaction inputs for (int i = 0; i < utxoSigningData.length; i++) { @@ -683,10 +670,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> txid.toUint8ListFromHex.reversed.toList(), ); - final prevOutpoint = coinlib.OutPoint( - hash, - utxoSigningData[i].utxo.vout, - ); + final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); final prevOutput = coinlib.Output.fromAddress( BigInt.from(utxoSigningData[i].utxo.value), @@ -746,9 +730,10 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> txid: utxoSigningData[i].utxo.txid, vout: utxoSigningData[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], + addresses: + utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], valueStringSats: utxoSigningData[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, @@ -803,10 +788,9 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", valueStringSats: txData.recipients![i].amount.raw.toString(), - addresses: [ - txData.recipients![i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [txData.recipients![i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -822,22 +806,33 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> // Sign the transaction accordingly for (int i = 0; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); - coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + final key = utxoSigningData[i].keyPair!.privateKey; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( internalKey: utxoSigningData[i].keyPair!.publicKey, ); - key = taproot.tweakPrivateKey(key); + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); } - - clTx = clTx.sign( - inputN: i, - value: value, - key: key, - prevOuts: prevOuts, - ); } } catch (e, s) { Logging.instance.e( @@ -879,17 +874,13 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> ); } - Future<TxData> prepareNameSend({ - required TxData txData, - }) async { + Future<TxData> prepareNameSend({required TxData txData}) async { try { if (txData.amount == null) { throw Exception("No recipients in attempted transaction!"); } - Logging.instance.t( - "prepareNameSend called with TxData:\n\n$txData", - ); + Logging.instance.t("prepareNameSend called with TxData:\n\n$txData"); final feeRateType = txData.feeRateType; final customSatsPerVByte = txData.satsPerVByte; @@ -944,20 +935,17 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> } final result = await coinSelectionName( - txData: txData.copyWith( - feeRateAmount: rate, - ), + txData: txData.copyWith(feeRateAmount: rate), utxos: utxos?.toList(), coinControl: coinControl, ); - Logging.instance.d( - "prepare send: $result", - ); + Logging.instance.d("prepare send: $result"); if (result.fee!.raw.toInt() < result.vSize!) { throw Exception( - "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " - "be less than vSize (${result.vSize})"); + "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " + "be less than vSize (${result.vSize})", + ); } return result; @@ -1028,19 +1016,20 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> final canCPFP = this is CpfpInterface && coinControl; - final spendableOutputs = availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); + final spendableOutputs = + availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )), + ) + .toList(); if (coinControl) { if (spendableOutputs.length < availableOutputs.length) { @@ -1050,8 +1039,9 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> } else { // sort spendable by age (oldest first) spendableOutputs.sort( - (a, b) => (b.blockTime ?? currentChainHeight) - .compareTo((a.blockTime ?? currentChainHeight)), + (a, b) => (b.blockTime ?? currentChainHeight).compareTo( + (a.blockTime ?? currentChainHeight), + ), ); } @@ -1061,8 +1051,10 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> spendableOutputs.insert(0, txData.opNameState!.output!); } - final spendableSatoshiValue = - spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); + final spendableSatoshiValue = spendableOutputs.fold( + BigInt.zero, + (p, e) => p + BigInt.from(e.value), + ); if (spendableSatoshiValue < satoshiAmountToSend) { throw Exception("Insufficient balance"); @@ -1082,21 +1074,24 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> final List<UTXO> utxoObjectsToUse = []; if (!coinControl) { - for (int i = 0; - satoshisBeingUsed < satoshiAmountToSend && - i < spendableOutputs.length; - i++) { + for ( + int i = 0; + satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; + i++ + ) { utxoObjectsToUse.add(spendableOutputs[i]); satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); inputsBeingConsumed += 1; } - for (int i = 0; - i < additionalOutputs && - inputsBeingConsumed < spendableOutputs.length; - i++) { + for ( + int i = 0; + i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; + i++ + ) { utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += - BigInt.from(spendableOutputs[inputsBeingConsumed].value); + satoshisBeingUsed += BigInt.from( + spendableOutputs[inputsBeingConsumed].value, + ); inputsBeingConsumed += 1; } } else { @@ -1120,17 +1115,17 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> final int vSizeForOneOutput; try { - vSizeForOneOutput = (await _createNameTx( - utxoSigningData: utxoSigningData, - isForFeeCalcPurposesOnly: true, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed], - ), - ), - )) - .vSize!; + vSizeForOneOutput = + (await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed], + ), + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForOneOutput: $e", error: e, stackTrace: s); rethrow; @@ -1141,23 +1136,20 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; try { - vSizeForTwoOutPuts = (await _createNameTx( - utxoSigningData: utxoSigningData, - isForFeeCalcPurposesOnly: true, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress, (await getCurrentChangeAddress())!.value], - [ - satoshiAmountToSend, - maxBI( - BigInt.zero, - satoshisBeingUsed - satoshiAmountToSend, + vSizeForTwoOutPuts = + (await _createNameTx( + utxoSigningData: utxoSigningData, + isForFeeCalcPurposesOnly: true, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress, (await getCurrentChangeAddress())!.value], + [ + satoshiAmountToSend, + maxBI(BigInt.zero, satoshisBeingUsed - satoshiAmountToSend), + ], ), - ], - ), - ), - )) - .vSize!; + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForTwoOutPuts: $e", error: e, stackTrace: s); rethrow; @@ -1168,18 +1160,18 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), ); // Assume 2 outputs, one for recipient and one for change final feeForTwoOutputs = BigInt.from( satsPerVByte != null ? (satsPerVByte * vSizeForTwoOutPuts) : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), ); Logging.instance.d( @@ -1250,12 +1242,14 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> recipientsArray.add(newChangeAddress); recipientsAmtArray.add(changeOutputSize); - Logging.instance.d('2 outputs in tx' - '\nInput size: $satoshisBeingUsed' - '\nRecipient output size: $satoshiAmountToSend' - '\nChange Output Size: $changeOutputSize' - '\nDifference (fee being paid): $feeBeingPaid sats' - '\nEstimated fee: $feeForTwoOutputs'); + Logging.instance.d( + '2 outputs in tx' + '\nInput size: $satoshisBeingUsed' + '\nRecipient output size: $satoshiAmountToSend' + '\nChange Output Size: $changeOutputSize' + '\nDifference (fee being paid): $feeBeingPaid sats' + '\nEstimated fee: $feeForTwoOutputs', + ); TxData txnData = await _createNameTx( utxoSigningData: utxoSigningData, @@ -1305,9 +1299,7 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize // is smaller than or equal to cryptoCurrency.dustLimit. Revert to single output transaction. - Logging.instance.d( - 'Reverting to 1 output in tx', - ); + Logging.instance.d('Reverting to 1 output in tx'); return await _singleOutputTxn(); } @@ -1365,7 +1357,4 @@ class NamecoinWallet<T extends ElectrumXCurrencyInterface> } } -enum NameState { - available, - unavailable; -} +enum NameState { available, unavailable } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart index 2660d60ff..e393ceb9d 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/electrumx_interface.dart @@ -36,7 +36,8 @@ import 'rbf_interface.dart'; import 'view_only_option_interface.dart'; mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> - on Bip39HDWallet<T> implements ViewOnlyOptionInterface<T> { + on Bip39HDWallet<T> + implements ViewOnlyOptionInterface<T> { late ElectrumXClient electrumXClient; late CachedElectrumXClient electrumXCachedClient; @@ -54,9 +55,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> try { _serverVersion ??= _parseServerVersion( - (await electrumXClient - .getServerFeatures() - .timeout(const Duration(seconds: 2)))["server_version"] as String, + (await electrumXClient.getServerFeatures().timeout( + const Duration(seconds: 2), + ))["server_version"] + as String, ); } catch (_) { // ignore failure as it doesn't matter @@ -74,32 +76,28 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } Future<List<({String address, Amount amount, bool isChange})>> - helperRecipientsConvert( - List<String> addrs, - List<BigInt> satValues, - ) async { + helperRecipientsConvert(List<String> addrs, List<BigInt> satValues) async { final List<({String address, Amount amount, bool isChange})> results = []; for (int i = 0; i < addrs.length; i++) { - results.add( - ( - address: addrs[i], - amount: Amount( - rawValue: satValues[i], - fractionDigits: cryptoCurrency.fractionDigits, - ), - isChange: (await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(AddressSubType.change) - .and() - .valueEqualTo(addrs[i]) - .valueProperty() - .findFirst()) != - null + results.add(( + address: addrs[i], + amount: Amount( + rawValue: satValues[i], + fractionDigits: cryptoCurrency.fractionDigits, ), - ); + isChange: + (await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(AddressSubType.change) + .and() + .valueEqualTo(addrs[i]) + .valueProperty() + .findFirst()) != + null, + )); } return results; @@ -133,21 +131,24 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final canCPFP = this is CpfpInterface && coinControl; - final spendableOutputs = availableOutputs - .where( - (e) => - !e.isBlocked && - (e.used != true) && - (canCPFP || - e.isConfirmed( - currentChainHeight, - cryptoCurrency.minConfirms, - cryptoCurrency.minCoinbaseConfirms, - )), - ) - .toList(); - final spendableSatoshiValue = - spendableOutputs.fold(BigInt.zero, (p, e) => p + BigInt.from(e.value)); + final spendableOutputs = + availableOutputs + .where( + (e) => + !e.isBlocked && + (e.used != true) && + (canCPFP || + e.isConfirmed( + currentChainHeight, + cryptoCurrency.minConfirms, + cryptoCurrency.minCoinbaseConfirms, + )), + ) + .toList(); + final spendableSatoshiValue = spendableOutputs.fold( + BigInt.zero, + (p, e) => p + BigInt.from(e.value), + ); if (spendableSatoshiValue < satoshiAmountToSend) { throw Exception("Insufficient balance"); @@ -165,45 +166,41 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } else { // sort spendable by age (oldest first) spendableOutputs.sort( - (a, b) => (b.blockTime ?? currentChainHeight) - .compareTo((a.blockTime ?? currentChainHeight)), + (a, b) => (b.blockTime ?? currentChainHeight).compareTo( + (a.blockTime ?? currentChainHeight), + ), ); } - Logging.instance.d( - "spendableOutputs.length: ${spendableOutputs.length}", - ); - Logging.instance.d( - "availableOutputs.length: ${availableOutputs.length}", - ); + Logging.instance.d("spendableOutputs.length: ${spendableOutputs.length}"); + Logging.instance.d("availableOutputs.length: ${availableOutputs.length}"); Logging.instance.d("spendableOutputs: $spendableOutputs"); - Logging.instance.d( - "spendableSatoshiValue: $spendableSatoshiValue", - ); - Logging.instance.d( - "satoshiAmountToSend: $satoshiAmountToSend", - ); + Logging.instance.d("spendableSatoshiValue: $spendableSatoshiValue"); + Logging.instance.d("satoshiAmountToSend: $satoshiAmountToSend"); BigInt satoshisBeingUsed = BigInt.zero; int inputsBeingConsumed = 0; final List<UTXO> utxoObjectsToUse = []; if (!coinControl) { - for (var i = 0; - satoshisBeingUsed < satoshiAmountToSend && - i < spendableOutputs.length; - i++) { + for ( + var i = 0; + satoshisBeingUsed < satoshiAmountToSend && i < spendableOutputs.length; + i++ + ) { utxoObjectsToUse.add(spendableOutputs[i]); satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); inputsBeingConsumed += 1; } - for (int i = 0; - i < additionalOutputs && - inputsBeingConsumed < spendableOutputs.length; - i++) { + for ( + int i = 0; + i < additionalOutputs && inputsBeingConsumed < spendableOutputs.length; + i++ + ) { utxoObjectsToUse.add(spendableOutputs[inputsBeingConsumed]); - satoshisBeingUsed += - BigInt.from(spendableOutputs[inputsBeingConsumed].value); + satoshisBeingUsed += BigInt.from( + spendableOutputs[inputsBeingConsumed].value, + ); inputsBeingConsumed += 1; } } else { @@ -213,9 +210,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } Logging.instance.d("satoshisBeingUsed: $satoshisBeingUsed"); - Logging.instance.d( - "inputsBeingConsumed: $inputsBeingConsumed", - ); + Logging.instance.d("inputsBeingConsumed: $inputsBeingConsumed"); Logging.instance.d('utxoObjectsToUse: $utxoObjectsToUse'); // numberOfOutputs' length must always be equal to that of recipientsArray and recipientsAmtArray @@ -245,16 +240,16 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final int vSizeForOneOutput; try { - vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], - ), - ), - )) - .vSize!; + vSizeForOneOutput = + (await buildTransaction( + utxoSigningData: utxoSigningData, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForOneOutput: $e", error: e, stackTrace: s); rethrow; @@ -265,22 +260,22 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> BigInt maxBI(BigInt a, BigInt b) => a > b ? a : b; try { - vSizeForTwoOutPuts = (await buildTransaction( - utxoSigningData: utxoSigningData, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress, (await getCurrentChangeAddress())!.value], - [ - satoshiAmountToSend, - maxBI( - BigInt.zero, - satoshisBeingUsed - (satoshiAmountToSend + BigInt.one), + vSizeForTwoOutPuts = + (await buildTransaction( + utxoSigningData: utxoSigningData, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress, (await getCurrentChangeAddress())!.value], + [ + satoshiAmountToSend, + maxBI( + BigInt.zero, + satoshisBeingUsed - (satoshiAmountToSend + BigInt.one), + ), + ], ), - ], - ), - ), - )) - .vSize!; + ), + )).vSize!; } catch (e, s) { Logging.instance.e("vSizeForTwoOutPuts: $e", error: e, stackTrace: s); rethrow; @@ -291,42 +286,30 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForOneOutput, + feeRatePerKB: selectedTxFeeRate, + ), ); // Assume 2 outputs, one for recipient and one for change final feeForTwoOutputs = BigInt.from( satsPerVByte != null ? (satsPerVByte * vSizeForTwoOutPuts) : estimateTxFee( - vSize: vSizeForTwoOutPuts, - feeRatePerKB: selectedTxFeeRate, - ), + vSize: vSizeForTwoOutPuts, + feeRatePerKB: selectedTxFeeRate, + ), ); - Logging.instance.d( - "feeForTwoOutputs: $feeForTwoOutputs", - ); - Logging.instance.d( - "feeForOneOutput: $feeForOneOutput", - ); + Logging.instance.d("feeForTwoOutputs: $feeForTwoOutputs"); + Logging.instance.d("feeForOneOutput: $feeForOneOutput"); final difference = satoshisBeingUsed - satoshiAmountToSend; Future<TxData> singleOutputTxn() async { - Logging.instance.d( - 'Input size: $satoshisBeingUsed', - ); - Logging.instance.d( - 'Recipient output size: $satoshiAmountToSend', - ); - Logging.instance.d( - 'Fee being paid: $difference sats', - ); - Logging.instance.d( - 'Estimated fee: $feeForOneOutput', - ); + Logging.instance.d('Input size: $satoshisBeingUsed'); + Logging.instance.d('Recipient output size: $satoshiAmountToSend'); + Logging.instance.d('Fee being paid: $difference sats'); + Logging.instance.d('Estimated fee: $feeForOneOutput'); final txnData = await buildTransaction( utxoSigningData: utxoSigningData, txData: txData.copyWith( @@ -406,9 +389,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> recipientsAmtArray.removeLast(); recipientsAmtArray.add(changeOutputSize); - Logging.instance.d( - 'Adjusted Input size: $satoshisBeingUsed', - ); + Logging.instance.d('Adjusted Input size: $satoshisBeingUsed'); Logging.instance.d( 'Adjusted Recipient output size: $satoshiAmountToSend', ); @@ -418,9 +399,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> Logging.instance.d( 'Adjusted Difference (fee being paid): $feeBeingPaid sats', ); - Logging.instance.d( - 'Adjusted Estimated fee: $feeForTwoOutputs', - ); + Logging.instance.d('Adjusted Estimated fee: $feeForTwoOutputs'); txnData = await buildTransaction( utxoSigningData: utxoSigningData, @@ -443,9 +422,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } else { // Something went wrong here. It either overshot or undershot the estimated fee amount or the changeOutputSize // is smaller than or equal to cryptoCurrency.dustLimit. Revert to single output transaction. - Logging.instance.d( - 'Reverting to 1 output in tx', - ); + Logging.instance.d('Reverting to 1 output in tx'); return await singleOutputTxn(); } @@ -466,36 +443,28 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> }) async { Logging.instance.d("Attempting to send all $cryptoCurrency"); if (txData.recipients!.length != 1) { - throw Exception( - "Send all to more than one recipient not yet supported", - ); + throw Exception("Send all to more than one recipient not yet supported"); } - final int vSizeForOneOutput = (await buildTransaction( - utxoSigningData: utxoSigningData, - txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [satoshisBeingUsed - BigInt.one], - ), - ), - )) - .vSize!; + final int vSizeForOneOutput = + (await buildTransaction( + utxoSigningData: utxoSigningData, + txData: txData.copyWith( + recipients: await helperRecipientsConvert( + [recipientAddress], + [satoshisBeingUsed - BigInt.one], + ), + ), + )).vSize!; BigInt feeForOneOutput = BigInt.from( satsPerVByte != null ? (satsPerVByte * vSizeForOneOutput) - : estimateTxFee( - vSize: vSizeForOneOutput, - feeRatePerKB: feeRatePerKB, - ), + : estimateTxFee(vSize: vSizeForOneOutput, feeRatePerKB: feeRatePerKB), ); if (satsPerVByte == null) { - final roughEstimate = roughFeeEstimate( - utxoSigningData.length, - 1, - feeRatePerKB, - ).raw; + final roughEstimate = + roughFeeEstimate(utxoSigningData.length, 1, feeRatePerKB).raw; if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -511,10 +480,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final data = await buildTransaction( txData: txData.copyWith( - recipients: await helperRecipientsConvert( - [recipientAddress], - [amount], - ), + recipients: await helperRecipientsConvert([recipientAddress], [amount]), ), utxoSigningData: utxoSigningData, ); @@ -528,23 +494,19 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> ); } - Future<List<SigningData>> fetchBuildTxData( - List<UTXO> utxosToUse, - ) async { + Future<List<SigningData>> fetchBuildTxData(List<UTXO> utxosToUse) async { // return data final List<SigningData> signingData = []; try { // Populating the addresses to check for (var i = 0; i < utxosToUse.length; i++) { - final derivePathType = - cryptoCurrency.addressType(address: utxosToUse[i].address!); + final derivePathType = cryptoCurrency.addressType( + address: utxosToUse[i].address!, + ); signingData.add( - SigningData( - derivePathType: derivePathType, - utxo: utxosToUse[i], - ), + SigningData(derivePathType: derivePathType, utxo: utxosToUse[i]), ); } @@ -564,9 +526,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final privateKey = await (this as PaynymInterface) .getPrivateKeyForPaynymReceivingAddress( - paymentCodeString: code!, - index: address.derivationIndex, - ); + paymentCodeString: code!, + index: address.derivationIndex, + ); keys = coinlib.HDPrivateKey.fromKeyAndChainCode( coinlib.ECPrivateKey.fromHex(privateKey.toHex), @@ -594,11 +556,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> return signingData; } catch (e, s) { - Logging.instance.e( - "fetchBuildTxData() threw", - error: e, - stackTrace: s, - ); + Logging.instance.e("fetchBuildTxData() threw", error: e, stackTrace: s); rethrow; } } @@ -623,9 +581,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> ); // TODO: [prio=high]: check this opt in rbf - final sequence = this is RbfInterface && (this as RbfInterface).flagOptInRBF - ? 0xffffffff - 10 - : 0xffffffff - 1; + final sequence = + this is RbfInterface && (this as RbfInterface).flagOptInRBF + ? 0xffffffff - 10 + : 0xffffffff - 1; // Add transaction inputs for (var i = 0; i < utxoSigningData.length; i++) { @@ -635,10 +594,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> txid.toUint8ListFromHex.reversed.toList(), ); - final prevOutpoint = coinlib.OutPoint( - hash, - utxoSigningData[i].utxo.vout, - ); + final prevOutpoint = coinlib.OutPoint(hash, utxoSigningData[i].utxo.vout); final prevOutput = coinlib.Output.fromAddress( BigInt.from(utxoSigningData[i].utxo.value), @@ -699,9 +655,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> txid: utxoSigningData[i].utxo.txid, vout: utxoSigningData[i].utxo.vout, ), - addresses: utxoSigningData[i].utxo.address == null - ? [] - : [utxoSigningData[i].utxo.address!], + addresses: + utxoSigningData[i].utxo.address == null + ? [] + : [utxoSigningData[i].utxo.address!], valueStringSats: utxoSigningData[i].utxo.value.toString(), witness: null, innerRedeemScriptAsm: null, @@ -741,10 +698,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> OutputV2.isarCantDoRequiredInDefaultConstructor( scriptPubKeyHex: "000000", valueStringSats: txData.recipients![i].amount.raw.toString(), - addresses: [ - txData.recipients![i].address.toString(), - ], - walletOwns: (await mainDB.isar.addresses + addresses: [txData.recipients![i].address.toString()], + walletOwns: + (await mainDB.isar.addresses .where() .walletIdEqualTo(walletId) .filter() @@ -760,22 +716,33 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> // Sign the transaction accordingly for (var i = 0; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); - coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + final key = utxoSigningData[i].keyPair!.privateKey; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( internalKey: utxoSigningData[i].keyPair!.publicKey, ); - key = taproot.tweakPrivateKey(key); + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); } - - clTx = clTx.sign( - inputN: i, - value: value, - key: key, - prevOuts: prevOuts, - ); } } catch (e, s) { Logging.instance.e( @@ -839,8 +806,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } Future<int> fetchTxCount({required String addressScriptHash}) async { - final transactions = - await electrumXClient.getHistory(scripthash: addressScriptHash); + final transactions = await electrumXClient.getHistory( + scripthash: addressScriptHash, + ); return transactions.length; } @@ -885,20 +853,21 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } Future<void> updateElectrumX() async { - final failovers = nodeService - .failoverNodesFor(currency: cryptoCurrency) - .map( - (e) => ElectrumXNode( - address: e.host, - port: e.port, - name: e.name, - id: e.id, - useSSL: e.useSSL, - torEnabled: e.torEnabled, - clearnetEnabled: e.clearnetEnabled, - ), - ) - .toList(); + final failovers = + nodeService + .failoverNodesFor(currency: cryptoCurrency) + .map( + (e) => ElectrumXNode( + address: e.host, + port: e.port, + name: e.name, + id: e.id, + useSSL: e.useSSL, + torEnabled: e.torEnabled, + clearnetEnabled: e.clearnetEnabled, + ), + ) + .toList(); final newNode = await _getCurrentElectrumXNode(); try { @@ -937,9 +906,11 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> int gapCounter = 0; int highestIndexWithHistory = 0; - for (int index = 0; - gapCounter < cryptoCurrency.maxUnusedAddressGap; - index += txCountBatchSize) { + for ( + int index = 0; + gapCounter < cryptoCurrency.maxUnusedAddressGap; + index += txCountBatchSize + ) { Logging.instance.d( "index: $index, \t GapCounter $chain ${type.name}: $gapCounter", ); @@ -985,9 +956,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> addressArray.add(address); - txCountCallArgs.add( - addressString, - ); + txCountCallArgs.add(addressString); } // get address tx counts @@ -1115,8 +1084,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } for (int i = 0; i < batches.length; i++) { - final response = - await electrumXClient.getBatchHistory(args: batches[i]!); + final response = await electrumXClient.getBatchHistory( + args: batches[i]!, + ); for (int j = 0; j < response.length; j++) { final entry = response[j]; for (int k = 0; k < entry.length; k++) { @@ -1149,15 +1119,16 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> return allTxHashes; } catch (e, s) { - Logging.instance - .e("$runtimeType._fetchHistory: ", error: e, stackTrace: s); + Logging.instance.e( + "$runtimeType._fetchHistory: ", + error: e, + stackTrace: s, + ); rethrow; } } - Future<UTXO> parseUTXO({ - required Map<String, dynamic> jsonUTXO, - }) async { + Future<UTXO> parseUTXO({required Map<String, dynamic> jsonUTXO}) async { final txn = await electrumXCachedClient.getTransaction( txHash: jsonUTXO["tx_hash"] as String, verbose: true, @@ -1179,7 +1150,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> scriptPubKey = output["scriptPubKey"]?["hex"] as String?; utxoOwnerAddress = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]?["address"] as String?; + output["scriptPubKey"]?["address"] as String?; } } @@ -1198,7 +1169,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> name: checkBlockResult.utxoLabel ?? "", isBlocked: checkBlockResult.blocked, blockedReason: checkBlockResult.blockedReason, - isCoinbase: txn["is_coinbase"] as bool? ?? + isCoinbase: + txn["is_coinbase"] as bool? ?? txn["is-coinbase"] as bool? ?? txn["iscoinbase"] as bool? ?? isCoinbase, @@ -1216,10 +1188,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> @override Future<void> updateChainHeight() async { final height = await fetchChainHeight(); - await info.updateCachedChainHeight( - newHeight: height, - isar: mainDB.isar, - ); + await info.updateCachedChainHeight(newHeight: height, isar: mainDB.isar); } @override @@ -1252,23 +1221,24 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Amount.fromDecimal( - fast, - fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), - medium: Amount.fromDecimal( - medium, - fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), - slow: Amount.fromDecimal( - slow, - fractionDigits: info.coin.fractionDigits, - ).raw.toInt(), + fast: + Amount.fromDecimal( + fast, + fractionDigits: info.coin.fractionDigits, + ).raw.toInt(), + medium: + Amount.fromDecimal( + medium, + fractionDigits: info.coin.fractionDigits, + ).raw.toInt(), + slow: + Amount.fromDecimal( + slow, + fractionDigits: info.coin.fractionDigits, + ).raw.toInt(), ); - Logging.instance.d( - "fetched fees: $feeObject", - ); + Logging.instance.d("fetched fees: $feeObject"); _cachedFees = feeObject; return _cachedFees!; } catch (e, s) { @@ -1383,9 +1353,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } } } catch (e, s) { - Logging.instance - .e("Exception rethrown from _checkReceivingAddressForTransactions" - "($cryptoCurrency): $e\n$s"); + Logging.instance.e( + "Exception rethrown from _checkReceivingAddressForTransactions" + "($cryptoCurrency): $e\n$s", + ); rethrow; } } @@ -1434,9 +1405,10 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> await checkChangeAddressForTransactions(); } } catch (e, s) { - Logging.instance - .e("Exception rethrown from _checkChangeAddressForTransactions" - "($cryptoCurrency): $e\n$s"); + Logging.instance.e( + "Exception rethrown from _checkChangeAddressForTransactions" + "($cryptoCurrency): $e\n$s", + ); rethrow; } } @@ -1472,47 +1444,25 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } // receiving addresses - Logging.instance.i( - "checking receiving addresses...", - ); + Logging.instance.i("checking receiving addresses..."); final canBatch = await serverCanBatch; for (final type in cryptoCurrency.supportedDerivationPathTypes) { receiveFutures.add( canBatch - ? checkGapsBatched( - txCountBatchSize, - root, - type, - receiveChain, - ) - : checkGapsLinearly( - root, - type, - receiveChain, - ), + ? checkGapsBatched(txCountBatchSize, root, type, receiveChain) + : checkGapsLinearly(root, type, receiveChain), ); } // change addresses - Logging.instance.d( - "checking change addresses...", - ); + Logging.instance.d("checking change addresses..."); for (final type in cryptoCurrency.supportedDerivationPathTypes) { changeFutures.add( canBatch - ? checkGapsBatched( - txCountBatchSize, - root, - type, - changeChain, - ) - : checkGapsLinearly( - root, - type, - changeChain, - ), + ? checkGapsBatched(txCountBatchSize, root, type, changeChain) + : checkGapsLinearly(root, type, changeChain), ); } @@ -1574,13 +1524,15 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final notificationAddress = await (this as PaynymInterface).getMyNotificationAddress(); - await (this as BitcoinWallet) - .updateTransactions(overrideAddresses: [notificationAddress]); + await (this as BitcoinWallet).updateTransactions( + overrideAddresses: [notificationAddress], + ); // get own payment code // isSegwit does not matter here at all - final myCode = - await (this as PaynymInterface).getPaymentCode(isSegwit: false); + final myCode = await (this as PaynymInterface).getPaymentCode( + isSegwit: false, + ); try { final Set<String> codesToCheck = {}; @@ -1648,8 +1600,9 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } for (int i = 0; i < batchArgs.length; i++) { - final response = - await electrumXClient.getBatchUTXOs(args: batchArgs[i]!); + final response = await electrumXClient.getBatchUTXOs( + args: batchArgs[i]!, + ); for (final entry in response) { if (entry.isNotEmpty) { fetchedUtxoList.add(entry); @@ -1673,9 +1626,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { - final utxo = await parseUTXO( - jsonUTXO: fetchedUtxoList[i][j], - ); + final utxo = await parseUTXO(jsonUTXO: fetchedUtxoList[i][j]); outputArray.add(utxo); } @@ -1695,16 +1646,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> @override Future<TxData> confirmSend({required TxData txData}) async { try { - Logging.instance.d( - "confirmSend txData: $txData", - ); + Logging.instance.d("confirmSend txData: $txData"); final txHash = await electrumXClient.broadcastTransaction( rawTx: txData.raw!, ); - Logging.instance.d( - "Sent txHash: $txHash", - ); + Logging.instance.d("Sent txHash: $txHash"); txData = txData.copyWith( usedUTXOs: @@ -1742,7 +1689,8 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> final bool coinControl = utxos != null; - final isSendAllCoinControlUtxos = coinControl && + final isSendAllCoinControlUtxos = + coinControl && txData.amount!.raw == utxos .map((e) => e.value) @@ -1811,22 +1759,19 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } final result = await coinSelection( - txData: txData.copyWith( - feeRateAmount: rate, - ), + txData: txData.copyWith(feeRateAmount: rate), isSendAll: isSendAll, utxos: utxos?.toList(), coinControl: coinControl, isSendAllCoinControlUtxos: isSendAllCoinControlUtxos, ); - Logging.instance.d( - "prepare send: $result", - ); + Logging.instance.d("prepare send: $result"); if (result.fee!.raw.toInt() < result.vSize!) { throw Exception( - "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " - "be less than vSize (${result.vSize})"); + "Error in fee calculation: Transaction fee (${result.fee!.raw.toInt()}) cannot " + "be less than vSize (${result.vSize})", + ); } return result; @@ -1862,16 +1807,15 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> Future<void> _initializeServerVersionAndCheckGenesisHash() async { try { - final features = await electrumXClient - .getServerFeatures() - .timeout(const Duration(seconds: 5)); - - Logging.instance.d( - "features: $features", + final features = await electrumXClient.getServerFeatures().timeout( + const Duration(seconds: 5), ); - _serverVersion = - _parseServerVersion(features["server_version"] as String); + Logging.instance.d("features: $features"); + + _serverVersion = _parseServerVersion( + features["server_version"] as String, + ); if (cryptoCurrency.genesisHash != features['genesis_hash']) { throw Exception("Genesis hash does not match!"); @@ -1896,7 +1840,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> /// Certain coins need to check if the utxo should be marked /// as blocked as well as give a reason. Future<({String? blockedReason, bool blocked, String? utxoLabel})> - checkBlockUTXO( + checkBlockUTXO( Map<String, dynamic> jsonUTXO, String? scriptPubKeyHex, Map<String, dynamic> jsonTX, @@ -1948,9 +1892,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> } } catch (_) {} - Logging.instance.d( - "${info.name} _parseServerVersion($version) => $result", - ); + Logging.instance.d("${info.name} _parseServerVersion($version) => $result"); return result; } @@ -2003,9 +1945,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> if (root != null) { // receiving addresses - Logging.instance.i( - "checking receiving addresses...", - ); + Logging.instance.i("checking receiving addresses..."); final canBatch = await serverCanBatch; @@ -2021,24 +1961,18 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> receiveFutures.add( canBatch ? checkGapsBatched( - txCountBatchSize, - root, - type, - receiveChain, - ) - : checkGapsLinearly( - root, - type, - receiveChain, - ), + txCountBatchSize, + root, + type, + receiveChain, + ) + : checkGapsLinearly(root, type, receiveChain), ); } } // change addresses - Logging.instance.d( - "checking change addresses...", - ); + Logging.instance.d("checking change addresses..."); for (final type in cryptoCurrency.supportedDerivationPathTypes) { final path = cryptoCurrency.constructDerivePath( derivePathType: type, @@ -2051,16 +1985,12 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface> changeFutures.add( canBatch ? checkGapsBatched( - txCountBatchSize, - root, - type, - changeChain, - ) - : checkGapsLinearly( - root, - type, - changeChain, - ), + txCountBatchSize, + root, + type, + changeChain, + ) + : checkGapsLinearly(root, type, changeChain), ); } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart index 9d1935bec..5c0ace879 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/paynym_interface.dart @@ -42,34 +42,28 @@ String _notificationDerivationPath({required bool testnet}) => String _receivingPaynymAddressDerivationPath( int index, { required bool testnet, -}) => - "${_basePaynymDerivePath(testnet: testnet)}/$index/0"; -String _sendPaynymAddressDerivationPath( - int index, { - required bool testnet, -}) => +}) => "${_basePaynymDerivePath(testnet: testnet)}/$index/0"; +String _sendPaynymAddressDerivationPath(int index, {required bool testnet}) => "${_basePaynymDerivePath(testnet: testnet)}/0/$index"; mixin PaynymInterface<T extends PaynymCurrencyInterface> on Bip39HDWallet<T>, ElectrumXInterface<T> { btc_dart.NetworkType get networkType => btc_dart.NetworkType( - messagePrefix: cryptoCurrency.networkParams.messagePrefix, - bech32: cryptoCurrency.networkParams.bech32Hrp, - bip32: btc_dart.Bip32Type( - public: cryptoCurrency.networkParams.pubHDPrefix, - private: cryptoCurrency.networkParams.privHDPrefix, - ), - pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, - scriptHash: cryptoCurrency.networkParams.p2shPrefix, - wif: cryptoCurrency.networkParams.wifPrefix, - ); + messagePrefix: cryptoCurrency.networkParams.messagePrefix, + bech32: cryptoCurrency.networkParams.bech32Hrp, + bip32: btc_dart.Bip32Type( + public: cryptoCurrency.networkParams.pubHDPrefix, + private: cryptoCurrency.networkParams.privHDPrefix, + ), + pubKeyHash: cryptoCurrency.networkParams.p2pkhPrefix, + scriptHash: cryptoCurrency.networkParams.p2shPrefix, + wif: cryptoCurrency.networkParams.wifPrefix, + ); Future<bip32.BIP32> getBip47BaseNode() async { final root = await _getRootNode(); final node = root.derivePath( - _basePaynymDerivePath( - testnet: info.coin.network.isTestNet, - ), + _basePaynymDerivePath(testnet: info.coin.network.isTestNet), ); return node; } @@ -101,25 +95,29 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> }) async { final keys = await lookupKey(sender.toString()); - final address = await mainDB - .getAddresses(walletId) - .filter() - .subTypeEqualTo(AddressSubType.paynymReceive) - .and() - .group((q) { - if (isSegwit) { - return q - .typeEqualTo(AddressType.p2sh) - .or() - .typeEqualTo(AddressType.p2wpkh); - } else { - return q.typeEqualTo(AddressType.p2pkh); - } - }) - .and() - .anyOf<String, Address>(keys, (q, String e) => q.otherDataEqualTo(e)) - .sortByDerivationIndexDesc() - .findFirst(); + final address = + await mainDB + .getAddresses(walletId) + .filter() + .subTypeEqualTo(AddressSubType.paynymReceive) + .and() + .group((q) { + if (isSegwit) { + return q + .typeEqualTo(AddressType.p2sh) + .or() + .typeEqualTo(AddressType.p2wpkh); + } else { + return q.typeEqualTo(AddressType.p2pkh); + } + }) + .and() + .anyOf<String, Address>( + keys, + (q, String e) => q.otherDataEqualTo(e), + ) + .sortByDerivationIndexDesc() + .findFirst(); if (address == null) { final generatedAddress = await _generatePaynymReceivingAddress( @@ -128,11 +126,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> generateSegwitAddress: isSegwit, ); - final existing = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(generatedAddress.value) - .findFirst(); + final existing = + await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(generatedAddress.value) + .findFirst(); if (existing == null) { // Add that new address @@ -142,10 +141,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> await mainDB.updateAddress(existing, generatedAddress); } - return currentReceivingPaynymAddress( - isSegwit: isSegwit, - sender: sender, - ); + return currentReceivingPaynymAddress(isSegwit: isSegwit, sender: sender); } else { return address; } @@ -158,9 +154,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> }) async { final root = await _getRootNode(); final node = root.derivePath( - _basePaynymDerivePath( - testnet: info.coin.network.isTestNet, - ), + _basePaynymDerivePath(testnet: info.coin.network.isTestNet), ); final paymentAddress = PaymentAddress( @@ -170,20 +164,22 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> index: 0, ); - final addressString = generateSegwitAddress - ? paymentAddress.getReceiveAddressP2WPKH() - : paymentAddress.getReceiveAddressP2PKH(); + final addressString = + generateSegwitAddress + ? paymentAddress.getReceiveAddressP2WPKH() + : paymentAddress.getReceiveAddressP2PKH(); final address = Address( walletId: walletId, value: addressString, publicKey: [], derivationIndex: index, - derivationPath: DerivationPath() - ..value = _receivingPaynymAddressDerivationPath( - index, - testnet: info.coin.network.isTestNet, - ), + derivationPath: + DerivationPath() + ..value = _receivingPaynymAddressDerivationPath( + index, + testnet: info.coin.network.isTestNet, + ), type: generateSegwitAddress ? AddressType.p2wpkh : AddressType.p2pkh, subType: AddressSubType.paynymReceive, otherData: await storeCode(sender.toString()), @@ -207,20 +203,22 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> index: index, ); - final addressString = generateSegwitAddress - ? paymentAddress.getSendAddressP2WPKH() - : paymentAddress.getSendAddressP2PKH(); + final addressString = + generateSegwitAddress + ? paymentAddress.getSendAddressP2WPKH() + : paymentAddress.getSendAddressP2PKH(); final address = Address( walletId: walletId, value: addressString, publicKey: [], derivationIndex: index, - derivationPath: DerivationPath() - ..value = _sendPaynymAddressDerivationPath( - index, - testnet: info.coin.network.isTestNet, - ), + derivationPath: + DerivationPath() + ..value = _sendPaynymAddressDerivationPath( + index, + testnet: info.coin.network.isTestNet, + ), type: AddressType.nonWallet, subType: AddressSubType.paynymSend, otherData: await storeCode(other.toString()), @@ -251,11 +249,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> generateSegwitAddress: isSegwit, ); - final existing = await mainDB - .getAddresses(walletId) - .filter() - .valueEqualTo(nextAddress.value) - .findFirst(); + final existing = + await mainDB + .getAddresses(walletId) + .filter() + .valueEqualTo(nextAddress.value) + .findFirst(); if (existing == null) { // Add that new address @@ -312,26 +311,18 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> Future<bip32.BIP32> deriveNotificationBip32Node() async { final root = await _getRootNode(); final node = root - .derivePath( - _basePaynymDerivePath( - testnet: info.coin.network.isTestNet, - ), - ) + .derivePath(_basePaynymDerivePath(testnet: info.coin.network.isTestNet)) .derive(0); return node; } /// fetch or generate this wallet's bip47 payment code - Future<PaymentCode> getPaymentCode({ - required bool isSegwit, - }) async { + Future<PaymentCode> getPaymentCode({required bool isSegwit}) async { final node = await _getRootNode(); final paymentCode = PaymentCode.fromBip32Node( node.derivePath( - _basePaynymDerivePath( - testnet: info.coin.network.isTestNet, - ), + _basePaynymDerivePath(testnet: info.coin.network.isTestNet), ), networkType: networkType, shouldSetSegwitBit: isSegwit, @@ -351,8 +342,9 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } Future<String> signStringWithNotificationKey(String data) async { - final bytes = - await signWithNotificationKey(Uint8List.fromList(utf8.encode(data))); + final bytes = await signWithNotificationKey( + Uint8List.fromList(utf8.encode(data)), + ); return Format.uint8listToString(bytes); } @@ -411,15 +403,19 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> for (int i = startIndex; i < maxCount; i++) { final keys = await lookupKey(pCode.toString()); - final address = await mainDB - .getAddresses(walletId) - .filter() - .subTypeEqualTo(AddressSubType.paynymSend) - .and() - .anyOf<String, Address>(keys, (q, String e) => q.otherDataEqualTo(e)) - .and() - .derivationIndexEqualTo(i) - .findFirst(); + final address = + await mainDB + .getAddresses(walletId) + .filter() + .subTypeEqualTo(AddressSubType.paynymSend) + .and() + .anyOf<String, Address>( + keys, + (q, String e) => q.otherDataEqualTo(e), + ) + .and() + .derivationIndexEqualTo(i) + .findFirst(); if (address != null) { final count = await fetchTxCount( @@ -506,21 +502,26 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> int outputsBeingUsed = 0; final List<UTXO> utxoObjectsToUse = []; - for (int i = 0; - satoshisBeingUsed < amountToSend.raw && i < spendableOutputs.length; - i++) { + for ( + int i = 0; + satoshisBeingUsed < amountToSend.raw && i < spendableOutputs.length; + i++ + ) { utxoObjectsToUse.add(spendableOutputs[i]); satoshisBeingUsed += BigInt.from(spendableOutputs[i].value); outputsBeingUsed += 1; } // add additional outputs if required - for (int i = 0; - i < additionalOutputs && outputsBeingUsed < spendableOutputs.length; - i++) { + for ( + int i = 0; + i < additionalOutputs && outputsBeingUsed < spendableOutputs.length; + i++ + ) { utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]); - satoshisBeingUsed += - BigInt.from(spendableOutputs[outputsBeingUsed].value); + satoshisBeingUsed += BigInt.from( + spendableOutputs[outputsBeingUsed].value, + ); outputsBeingUsed += 1; } @@ -534,8 +535,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> change: BigInt.zero, // override amount to get around absurd fees error overrideAmountForTesting: satoshisBeingUsed, - )) - .item2, + )).item2, ); final vSizeForWithChange = BigInt.from( @@ -543,8 +543,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> targetPaymentCodeString: targetPaymentCodeString, utxoSigningData: utxoSigningData, change: satoshisBeingUsed - amountToSend.raw, - )) - .item2, + )).item2, ); // Assume 2 outputs, for recipient and payment code script @@ -836,10 +835,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> clTx = clTx.addOutput(output); clTx = clTx.addOutput( - coinlib.Output.fromScriptBytes( - BigInt.zero, - opReturnScript, - ), + coinlib.Output.fromScriptBytes(BigInt.zero, opReturnScript), ); // TODO: add possible change output and mark output as dangerous @@ -859,32 +855,64 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> clTx = clTx.addOutput(output); } - clTx = clTx.sign( - inputN: 0, - value: BigInt.from(utxo.value), - key: myKeyPair.privateKey, - prevOuts: prevOuts, - ); + if (clTx.inputs[0] is coinlib.TaprootKeyInput) { + final taproot = coinlib.Taproot(internalKey: myKeyPair.publicKey); + + clTx = clTx.signTaproot( + inputN: 0, + key: taproot.tweakPrivateKey(myKeyPair.privateKey), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[0] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness( + inputN: 0, + key: myKeyPair.privateKey, + value: BigInt.from(utxo.value), + ); + } else if (clTx.inputs[0] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: 0, key: myKeyPair.privateKey); + } else if (clTx.inputs[0] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: 0, + key: myKeyPair.privateKey, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[0].runtimeType}", + ); + } // sign rest of possible inputs for (int i = 1; i < utxoSigningData.length; i++) { final value = BigInt.from(utxoSigningData[i].utxo.value); - coinlib.ECPrivateKey key = utxoSigningData[i].keyPair!.privateKey; + final key = utxoSigningData[i].keyPair!.privateKey; if (clTx.inputs[i] is coinlib.TaprootKeyInput) { final taproot = coinlib.Taproot( internalKey: utxoSigningData[i].keyPair!.publicKey, ); - key = taproot.tweakPrivateKey(key); + clTx = clTx.signTaproot( + inputN: i, + key: taproot.tweakPrivateKey(key), + prevOuts: prevOuts, + ); + } else if (clTx.inputs[i] is coinlib.LegacyWitnessInput) { + clTx = clTx.signLegacyWitness(inputN: i, key: key, value: value); + } else if (clTx.inputs[i] is coinlib.LegacyInput) { + clTx = clTx.signLegacy(inputN: i, key: key); + } else if (clTx.inputs[i] is coinlib.TaprootSingleScriptSigInput) { + clTx = clTx.signTaprootSingleScriptSig( + inputN: i, + key: key, + prevOuts: prevOuts, + ); + } else { + throw Exception( + "Unable to sign input of type ${clTx.inputs[i].runtimeType}", + ); } - - clTx = clTx.sign( - inputN: i, - value: value, - key: key, - prevOuts: prevOuts, - ); } return Tuple2(clTx.toHex(), clTx.vSize()); @@ -894,13 +922,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } } - Future<TxData> broadcastNotificationTx({ - required TxData txData, - }) async { + Future<TxData> broadcastNotificationTx({required TxData txData}) async { try { Logging.instance.d("confirmNotificationTx txData: $txData"); - final txHash = - await electrumXClient.broadcastTransaction(rawTx: txData.raw!); + final txHash = await electrumXClient.broadcastTransaction( + rawTx: txData.raw!, + ); Logging.instance.d("Sent txHash: $txHash"); try { @@ -913,10 +940,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> ); } - return txData.copyWith( - txid: txHash, - txHash: txHash, - ); + return txData.copyWith(txid: txHash, txHash: txHash); } catch (e, s) { Logging.instance.e( "Exception rethrown from confirmSend(): ", @@ -964,12 +988,13 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> final myNotificationAddress = await getMyNotificationAddress(); - final txns = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(TransactionSubType.bip47Notification) - .findAll(); + final txns = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(TransactionSubType.bip47Notification) + .findAll(); for (final tx in txns) { switch (tx.type) { @@ -978,9 +1003,7 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> for (final outputAddress in output.addresses) { if (outputAddress == myNotificationAddress.value) { final unBlindedPaymentCode = - await unBlindedPaymentCodeFromTransaction( - transaction: tx, - ); + await unBlindedPaymentCodeFromTransaction(transaction: tx); if (unBlindedPaymentCode != null && paymentCodeString == unBlindedPaymentCode.toString()) { @@ -990,8 +1013,8 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> final unBlindedPaymentCodeBad = await unBlindedPaymentCodeFromTransactionBad( - transaction: tx, - ); + transaction: tx, + ); if (unBlindedPaymentCodeBad != null && paymentCodeString == unBlindedPaymentCodeBad.toString()) { @@ -1005,14 +1028,15 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> case TransactionType.outgoing: for (final output in tx.outputs) { for (final outputAddress in output.addresses) { - final address = await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(AddressSubType.paynymNotification) - .and() - .valueEqualTo(outputAddress) - .findFirst(); + final address = + await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(AddressSubType.paynymNotification) + .and() + .valueEqualTo(outputAddress) + .findFirst(); if (address?.otherData != null) { final code = await paymentCodeStringByKey(address!.otherData!); @@ -1055,8 +1079,9 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> required TransactionV2 transaction, }) async { try { - final blindedCodeBytes = - Bip47Utils.getBlindedPaymentCodeBytesFrom(transaction); + final blindedCodeBytes = Bip47Utils.getBlindedPaymentCodeBytesFrom( + transaction, + ); // transaction does not contain a payment code if (blindedCodeBytes == null) { @@ -1113,8 +1138,9 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> required TransactionV2 transaction, }) async { try { - final blindedCodeBytes = - Bip47Utils.getBlindedPaymentCodeBytesFrom(transaction); + final blindedCodeBytes = Bip47Utils.getBlindedPaymentCodeBytesFrom( + transaction, + ); // transaction does not contain a payment code if (blindedCodeBytes == null) { @@ -1168,13 +1194,14 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } Future<List<PaymentCode>> - getAllPaymentCodesFromNotificationTransactions() async { - final txns = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(TransactionSubType.bip47Notification) - .findAll(); + getAllPaymentCodesFromNotificationTransactions() async { + final txns = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(TransactionSubType.bip47Notification) + .findAll(); final List<PaymentCode> codes = []; @@ -1182,20 +1209,23 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> // tx is sent so we can check the address's otherData for the code String if (tx.type == TransactionType.outgoing) { for (final output in tx.outputs) { - for (final outputAddress - in output.addresses.where((e) => e.isNotEmpty)) { - final address = await mainDB.isar.addresses - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(AddressSubType.paynymNotification) - .and() - .valueEqualTo(outputAddress) - .findFirst(); + for (final outputAddress in output.addresses.where( + (e) => e.isNotEmpty, + )) { + final address = + await mainDB.isar.addresses + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(AddressSubType.paynymNotification) + .and() + .valueEqualTo(outputAddress) + .findFirst(); if (address?.otherData != null) { - final codeString = - await paymentCodeStringByKey(address!.otherData!); + final codeString = await paymentCodeStringByKey( + address!.otherData!, + ); if (codeString != null && codes.where((e) => e.toString() == codeString).isEmpty) { codes.add( @@ -1236,14 +1266,15 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> Future<void> checkForNotificationTransactionsTo( Set<String> otherCodeStrings, ) async { - final sentNotificationTransactions = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .subTypeEqualTo(TransactionSubType.bip47Notification) - .and() - .typeEqualTo(TransactionType.outgoing) - .findAll(); + final sentNotificationTransactions = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .subTypeEqualTo(TransactionSubType.bip47Notification) + .and() + .typeEqualTo(TransactionType.outgoing) + .findAll(); final List<PaymentCode> codes = []; for (final codeString in otherCodeStrings) { @@ -1260,8 +1291,10 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> final notificationAddress = code.notificationAddressP2PKH(); if (outputAddress == notificationAddress) { - Address? storedAddress = - await mainDB.getAddress(walletId, outputAddress); + Address? storedAddress = await mainDB.getAddress( + walletId, + outputAddress, + ); if (storedAddress == null) { // most likely not mine storedAddress = Address( @@ -1343,10 +1376,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> int outgoingGapCounter = 0; // non segwit receiving - for (int i = 0; - i < maxNumberOfIndexesToCheck && - receivingGapCounter < maxUnusedAddressGap; - i++) { + for ( + int i = 0; + i < maxNumberOfIndexesToCheck && + receivingGapCounter < maxUnusedAddressGap; + i++ + ) { if (receivingGapCounter < maxUnusedAddressGap) { final address = await _generatePaynymReceivingAddress( sender: other, @@ -1371,10 +1406,11 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } // non segwit sends - for (int i = 0; - i < maxNumberOfIndexesToCheck && - outgoingGapCounter < maxUnusedAddressGap; - i++) { + for ( + int i = 0; + i < maxNumberOfIndexesToCheck && outgoingGapCounter < maxUnusedAddressGap; + i++ + ) { if (outgoingGapCounter < maxUnusedAddressGap) { final address = await _generatePaynymSendAddress( other: other, @@ -1403,10 +1439,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> int receivingGapCounterSegwit = 0; int outgoingGapCounterSegwit = 0; // segwit receiving - for (int i = 0; - i < maxNumberOfIndexesToCheck && - receivingGapCounterSegwit < maxUnusedAddressGap; - i++) { + for ( + int i = 0; + i < maxNumberOfIndexesToCheck && + receivingGapCounterSegwit < maxUnusedAddressGap; + i++ + ) { if (receivingGapCounterSegwit < maxUnusedAddressGap) { final address = await _generatePaynymReceivingAddress( sender: other, @@ -1431,10 +1469,12 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } // segwit sends - for (int i = 0; - i < maxNumberOfIndexesToCheck && - outgoingGapCounterSegwit < maxUnusedAddressGap; - i++) { + for ( + int i = 0; + i < maxNumberOfIndexesToCheck && + outgoingGapCounterSegwit < maxUnusedAddressGap; + i++ + ) { if (outgoingGapCounterSegwit < maxUnusedAddressGap) { final address = await _generatePaynymSendAddress( other: other, @@ -1463,25 +1503,24 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } Future<Address> getMyNotificationAddress() async { - final storedAddress = await mainDB - .getAddresses(walletId) - .filter() - .subTypeEqualTo(AddressSubType.paynymNotification) - .and() - .typeEqualTo(AddressType.p2pkh) - .and() - .not() - .typeEqualTo(AddressType.nonWallet) - .findFirst(); + final storedAddress = + await mainDB + .getAddresses(walletId) + .filter() + .subTypeEqualTo(AddressSubType.paynymNotification) + .and() + .typeEqualTo(AddressType.p2pkh) + .and() + .not() + .typeEqualTo(AddressType.nonWallet) + .findFirst(); if (storedAddress != null) { return storedAddress; } else { final root = await _getRootNode(); final node = root.derivePath( - _basePaynymDerivePath( - testnet: info.coin.network.isTestNet, - ), + _basePaynymDerivePath(testnet: info.coin.network.isTestNet), ); final paymentCode = PaymentCode.fromBip32Node( node, @@ -1493,23 +1532,19 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> pubkey: paymentCode.notificationPublicKey(), ); - final addressString = btc_dart - .P2PKH( - data: data, - network: networkType, - ) - .data - .address!; + final addressString = + btc_dart.P2PKH(data: data, network: networkType).data.address!; Address address = Address( walletId: walletId, value: addressString, publicKey: paymentCode.getPubKey(), derivationIndex: 0, - derivationPath: DerivationPath() - ..value = _notificationDerivationPath( - testnet: info.coin.network.isTestNet, - ), + derivationPath: + DerivationPath() + ..value = _notificationDerivationPath( + testnet: info.coin.network.isTestNet, + ), type: AddressType.p2pkh, subType: AddressSubType.paynymNotification, otherData: await storeCode(paymentCode.toString()), @@ -1520,16 +1555,17 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> // beginning to see if there already was notification address. This would // lead to a Unique Index violation error await mainDB.isar.writeTxn(() async { - final storedAddress = await mainDB - .getAddresses(walletId) - .filter() - .subTypeEqualTo(AddressSubType.paynymNotification) - .and() - .typeEqualTo(AddressType.p2pkh) - .and() - .not() - .typeEqualTo(AddressType.nonWallet) - .findFirst(); + final storedAddress = + await mainDB + .getAddresses(walletId) + .filter() + .subTypeEqualTo(AddressSubType.paynymNotification) + .and() + .typeEqualTo(AddressType.p2pkh) + .and() + .not() + .typeEqualTo(AddressType.nonWallet) + .findFirst(); if (storedAddress == null) { await mainDB.isar.addresses.put(address); @@ -1607,45 +1643,43 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> overrideAddresses ?? await fetchAddressesForElectrumXScan(); // Separate receiving and change addresses. - final Set<String> receivingAddresses = allAddressesOld - .where( - (e) => - e.subType == AddressSubType.receiving || - e.subType == AddressSubType.paynymNotification || - e.subType == AddressSubType.paynymReceive, - ) - .map((e) => e.value) - .toSet(); - final Set<String> changeAddresses = allAddressesOld - .where((e) => e.subType == AddressSubType.change) - .map((e) => e.value) - .toSet(); + final Set<String> receivingAddresses = + allAddressesOld + .where( + (e) => + e.subType == AddressSubType.receiving || + e.subType == AddressSubType.paynymNotification || + e.subType == AddressSubType.paynymReceive, + ) + .map((e) => e.value) + .toSet(); + final Set<String> changeAddresses = + allAddressesOld + .where((e) => e.subType == AddressSubType.change) + .map((e) => e.value) + .toSet(); // Remove duplicates. final allAddressesSet = {...receivingAddresses, ...changeAddresses}; // Fetch history from ElectrumX. - final List<Map<String, dynamic>> allTxHashes = - await fetchHistory(allAddressesSet); - - final unconfirmedTxs = await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .heightIsNull() - .or() - .heightEqualTo(0) - .txidProperty() - .findAll(); - - allTxHashes.addAll( - unconfirmedTxs.map( - (e) => { - "tx_hash": e, - }, - ), + final List<Map<String, dynamic>> allTxHashes = await fetchHistory( + allAddressesSet, ); + final unconfirmedTxs = + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .heightIsNull() + .or() + .heightEqualTo(0) + .txidProperty() + .findAll(); + + allTxHashes.addAll(unconfirmedTxs.map((e) => {"tx_hash": e})); + // Only parse new txs (not in db yet). final List<Map<String, dynamic>> allTransactions = []; for (final txHash in allTxHashes) { @@ -1670,16 +1704,17 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } catch (e) { // tx no longer exists then delete from local db if (e.toString().contains( - "JSON-RPC error 2: daemon error: DaemonError({'code': -5, " - "'message': 'No such mempool or blockchain transaction", - )) { + "JSON-RPC error 2: daemon error: DaemonError({'code': -5, " + "'message': 'No such mempool or blockchain transaction", + )) { await mainDB.isar.writeTxn( - () async => await mainDB.isar.transactionV2s - .where() - .walletIdEqualTo(walletId) - .filter() - .txidEqualTo(txid) - .deleteFirst(), + () async => + await mainDB.isar.transactionV2s + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txid) + .deleteFirst(), ); continue; } else { @@ -1802,8 +1837,9 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> TransactionSubType subType = TransactionSubType.none; if (outputs.length > 1 && inputs.isNotEmpty) { for (int i = 0; i < outputs.length; i++) { - final List<String>? scriptChunks = - outputs[i].scriptPubKeyAsm?.split(" "); + final List<String>? scriptChunks = outputs[i].scriptPubKeyAsm?.split( + " ", + ); if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") { final blindedPaymentCode = scriptChunks![1]; final bytes = blindedPaymentCode.toUint8ListFromHex; @@ -1856,7 +1892,8 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> txid: txData["txid"] as String, height: txData["height"] as int?, version: txData["version"] as int, - timestamp: txData["blocktime"] as int? ?? + timestamp: + txData["blocktime"] as int? ?? DateTime.timestamp().millisecondsSinceEpoch ~/ 1000, inputs: List.unmodifiable(inputs), outputs: List.unmodifiable(outputs), @@ -1872,12 +1909,8 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> } @override - Future< - ({ - String? blockedReason, - bool blocked, - String? utxoLabel, - })> checkBlockUTXO( + Future<({String? blockedReason, bool blocked, String? utxoLabel})> + checkBlockUTXO( Map<String, dynamic> jsonUTXO, String? scriptPubKeyHex, Map<String, dynamic>? jsonTX, @@ -1905,7 +1938,8 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> blocked = true; blockedReason = "Incoming paynym notification transaction."; } else { - blockedReason = "Paynym notification change output. Incautious " + blockedReason = + "Paynym notification change output. Incautious " "handling of change outputs from notification transactions " "may cause unintended loss of privacy."; utxoLabel = blockedReason; @@ -1920,23 +1954,21 @@ mixin PaynymInterface<T extends PaynymCurrencyInterface> return ( blockedReason: blockedReason, blocked: blocked, - utxoLabel: utxoLabel + utxoLabel: utxoLabel, ); } @override FilterOperation? get transactionFilterOperation => FilterGroup.not( - const FilterGroup.and( - [ - FilterCondition.equalTo( - property: r"subType", - value: TransactionSubType.bip47Notification, - ), - FilterCondition.equalTo( - property: r"type", - value: TransactionType.incoming, - ), - ], - ), - ); + const FilterGroup.and([ + FilterCondition.equalTo( + property: r"subType", + value: TransactionSubType.bip47Notification, + ), + FilterCondition.equalTo( + property: r"type", + value: TransactionType.incoming, + ), + ]), + ); } diff --git a/scripts/app_config/templates/pubspec.template b/scripts/app_config/templates/pubspec.template index 62b1947ca..7f9d25c86 100644 --- a/scripts/app_config/templates/pubspec.template +++ b/scripts/app_config/templates/pubspec.template @@ -38,7 +38,7 @@ dependencies: flutter_libsparkmobile: git: url: https://github.com/cypherstack/flutter_libsparkmobile.git - ref: ca0c72cecc40fc0bfbafc0d26af675d973ab516b + ref: 33e1034911b842c57bdf6ddaa825cbf635a0c9db # cs_monero compat (unpublished) compat: @@ -176,12 +176,7 @@ dependencies: convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 -# coinlib_flutter: ^2.1.0-rc.1 - coinlib_flutter: - git: - url: https://github.com/julian-CStack/coinlib.git - ref: 0acacfd17eacf72135c693a7b862bd9b7cc56739 - path: coinlib_flutter + coinlib_flutter: ^3.0.0 electrum_adapter: git: url: https://github.com/cypherstack/electrum_adapter.git @@ -261,18 +256,9 @@ dependency_overrides: # needed for dart 3.5+ (at least for now) win32: ^5.5.4 - # coin lib git for testing while waiting for publishing - coinlib: - git: - url: https://github.com/julian-CStack/coinlib.git - ref: 0acacfd17eacf72135c693a7b862bd9b7cc56739 - path: coinlib - - coinlib_flutter: - git: - url: https://github.com/julian-CStack/coinlib.git - ref: 0acacfd17eacf72135c693a7b862bd9b7cc56739 - path: coinlib_flutter + # namecoin names lib needs to be updated + coinlib: ^3.0.0 + coinlib_flutter: ^3.0.0 bip47: git: