feat: tx broadcast, error handling, remove electrum.dart

This commit is contained in:
Rafael Saes 2024-12-19 13:18:58 -03:00
parent 6cbb4c60d6
commit 51590a668d
10 changed files with 728 additions and 832 deletions

View file

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
abstract class BaseBitcoinAddressRecord {
@ -84,11 +85,7 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
throw ArgumentError('either scriptHash or network must be provided');
}
try {
this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
} catch (_) {
this.scriptHash = '';
}
}
factory BitcoinAddressRecord.fromJSON(String jsonSource) {
@ -211,7 +208,7 @@ class BitcoinSilentPaymentAddressRecord extends BaseBitcoinAddressRecord {
}
class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
final ECPrivate spendKey;
final String tweak;
BitcoinReceivedSPAddressRecord(
super.address, {
@ -220,11 +217,32 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
super.balance = 0,
super.name = '',
super.isUsed = false,
required this.spendKey,
required this.tweak,
super.addressType = SegwitAddresType.p2tr,
super.labelHex,
}) : super(isHidden: true);
SilentPaymentOwner getSPWallet(
List<SilentPaymentOwner> silentPaymentsWallets, [
BasedUtxoNetwork network = BitcoinNetwork.mainnet,
]) {
final spAddress = silentPaymentsWallets.firstWhere(
(wallet) => wallet.toAddress(network) == this.address,
orElse: () => throw ArgumentError('SP wallet not found'),
);
return spAddress;
}
ECPrivate getSpendKey(
List<SilentPaymentOwner> silentPaymentsWallets, [
BasedUtxoNetwork network = BitcoinNetwork.mainnet,
]) {
return getSPWallet(silentPaymentsWallets, network)
.b_spend
.tweakAdd(BigintUtils.fromBytes(BytesUtils.fromHexString(tweak)));
}
factory BitcoinReceivedSPAddressRecord.fromJSON(String jsonSource, {BasedUtxoNetwork? network}) {
final decoded = json.decode(jsonSource) as Map;
@ -236,9 +254,7 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
name: decoded['name'] as String? ?? '',
balance: decoded['balance'] as int? ?? 0,
labelHex: decoded['label'] as String?,
spendKey: (decoded['spendKey'] as String?) == null
? ECPrivate.random()
: ECPrivate.fromHex(decoded['spendKey'] as String),
tweak: decoded['tweak'] as String? ?? '',
);
}
@ -252,6 +268,6 @@ class BitcoinReceivedSPAddressRecord extends BitcoinSilentPaymentAddressRecord {
'balance': balance,
'type': addressType.toString(),
'labelHex': labelHex,
'spend_key': spendKey.toString(),
'tweak': tweak,
});
}

View file

@ -4,9 +4,11 @@ import 'dart:convert';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:cw_bitcoin/pending_bitcoin_transaction.dart';
import 'package:cw_bitcoin/psbt_transaction_builder.dart';
// import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
@ -17,15 +19,14 @@ import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
import 'package:cw_core/crypto_currency.dart';
// import 'package:cw_core/get_height_by_date.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
// import 'package:cw_core/wallet_type.dart';
// import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_bitcoin/ledger_bitcoin.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
@ -251,7 +252,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final passphrase = keysData.passphrase;
if (mnemonic != null) {
for (final derivation in walletInfo.derivations ?? <DerivationInfo>[]) {
final derivations = walletInfo.derivations ?? <DerivationInfo>[];
for (final derivation in derivations) {
if (derivation.description?.contains("SP") ?? false) {
continue;
}
@ -289,6 +292,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
hdWallets[CWBitcoinDerivationType.electrum]!;
}
if (derivations.isEmpty) {
switch (walletInfo.derivationInfo?.derivationType) {
case DerivationType.bip39:
seedBytes = await Bip39SeedGenerator.generateFromString(mnemonic, passphrase);
@ -301,6 +305,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
break;
}
}
}
return BitcoinWallet(
mnemonic: mnemonic,
@ -914,4 +919,566 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
super.syncStatusReaction(syncStatus);
}
}
@override
Future<int> calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
required int feeRate,
List<ECPrivateInfo>? inputPrivKeyInfos,
List<Outpoint>? vinOutpoints,
}) async =>
feeRate *
BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
outputs: outputs,
network: network,
memo: memo,
inputPrivKeyInfos: inputPrivKeyInfos,
vinOutpoints: vinOutpoints,
);
@override
TxCreateUtxoDetails createUTXOS({
required bool sendAll,
bool paysToSilentPayment = false,
int credentialsAmount = 0,
int? inputsCount,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) {
List<UtxoWithAddress> utxos = [];
List<Outpoint> vinOutpoints = [];
List<ECPrivateInfo> inputPrivKeyInfos = [];
final publicKeys = <String, PublicKeyWithDerivationPath>{};
int allInputsAmount = 0;
bool spendsSilentPayment = false;
bool spendsUnconfirmedTX = false;
int leftAmount = credentialsAmount;
var availableInputs = unspentCoins.where((utx) {
if (!utx.isSending || utx.isFrozen) {
return false;
}
return true;
}).toList();
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
for (int i = 0; i < availableInputs.length; i++) {
final utx = availableInputs[i];
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
if (paysToSilentPayment) {
// Check inputs for shared secret derivation
if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) {
throw BitcoinTransactionSilentPaymentsNotSupported();
}
}
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = RegexUtils.addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
if (utx.bitcoinAddressRecord is BitcoinSilentPaymentAddressRecord) {
privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).getSpendKey(
(walletAddresses as BitcoinWalletAddresses).silentPaymentWallets,
network,
);
spendsSilentPayment = true;
isSilentPayment = true;
} else if (!isHardwareWallet) {
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
final path = addressRecord.derivationInfo.derivationPath
.addElem(Bip32KeyIndex(
BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange),
))
.addElem(Bip32KeyIndex(addressRecord.index));
privkey = ECPrivate.fromBip32(bip32: bip32.derive(path));
}
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 = walletAddresses.hdWallet
.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index))
.publicKey
.toHex();
}
if (utx.bitcoinAddressRecord is BitcoinAddressRecord) {
final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord)
.derivationInfo
.derivationPath
.toString();
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
}
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utx.hash,
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: BitcoinAddressUtils.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 TxCreateUtxoDetails(
availableInputs: availableInputs,
unconfirmedCoins: unconfirmedCoins,
utxos: utxos,
vinOutpoints: vinOutpoints,
inputPrivKeyInfos: inputPrivKeyInfos,
publicKeys: publicKeys,
allInputsAmount: allInputsAmount,
spendsSilentPayment: spendsSilentPayment,
spendsUnconfirmedTX: spendsUnconfirmedTX,
);
}
@override
Future<EstimatedTxResult> estimateSendAllTx(
List<BitcoinOutput> outputs,
int feeRate, {
String? memo,
bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
final utxoDetails = createUTXOS(sendAll: true, paysToSilentPayment: hasSilentPayment);
int fee = await calcFee(
utxos: utxoDetails.utxos,
outputs: outputs,
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,
);
}
@override
Future<EstimatedTxResult> estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
List<BitcoinOutput> updatedOutputs = const [],
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,
);
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,
feeRate,
updatedOutputs: updatedOutputs,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
hasSilentPayment: hasSilentPayment,
);
}
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress(
inputs: utxoDetails.availableInputs,
outputs: updatedOutputs,
);
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 = changeAddress.derivationInfo.derivationPath.toString();
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,
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,
feeRate,
updatedOutputs: updatedOutputs,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
hasSilentPayment: hasSilentPayment,
);
} else {
throw BitcoinTransactionWrongBalanceException();
}
}
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,
);
}
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
final outputs = <BitcoinOutput>[];
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,
feeRateInt,
updatedOutputs: updatedOutputs,
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,
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
});
}
final 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)}";
try {
key = estimatedTx.inputPrivKeyInfos.firstWhere((element) {
final elemPubkey = element.privkey.getPublic().toHex();
if (elemPubkey == publicKey) {
return true;
} else {
error += "\nExpected: $publicKey";
error += "\nPubkey: $elemPubkey";
return false;
}
});
} catch (_) {
throw Exception(error);
}
}
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,
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
hasSilentPayment: hasSilentPayment,
)..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();
});
} catch (e, s) {
print([e, s]);
throw e;
}
}
}

View file

@ -1,621 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/foundation.dart';
import 'package:rxdart/rxdart.dart';
String jsonrpc(
{required String method,
required List<Object> params,
required int id,
double version = 2.0}) =>
'{"jsonrpc": "$version", "method": "$method", "id": "$id", "params": ${json.encode(params)}}\n';
class SocketTask {
SocketTask({required this.isSubscription, this.completer, this.subject});
final Completer<dynamic>? completer;
final BehaviorSubject<dynamic>? subject;
final bool isSubscription;
}
class ElectrumClient {
ElectrumClient()
: _id = 0,
_isConnected = false,
_tasks = {},
_errors = {},
unterminatedString = '';
static const connectionTimeout = Duration(seconds: 5);
static const aliveTimerDuration = Duration(seconds: 4);
bool get isConnected => _isConnected;
Socket? socket;
void Function(ConnectionStatus)? onConnectionStatusChange;
int _id;
final Map<String, SocketTask> _tasks;
Map<String, SocketTask> get tasks => _tasks;
final Map<String, String> _errors;
ConnectionStatus _connectionStatus = ConnectionStatus.disconnected;
bool _isConnected;
Timer? _aliveTimer;
String unterminatedString;
Uri? uri;
bool? useSSL;
Future<void> connectToUri(Uri uri, {bool? useSSL}) async {
this.uri = uri;
if (useSSL != null) {
this.useSSL = useSSL;
}
await connect(host: uri.host, port: uri.port);
}
Future<void> connect({required String host, required int port}) async {
_setConnectionStatus(ConnectionStatus.connecting);
try {
await socket?.close();
} catch (_) {}
socket = null;
try {
if (useSSL == false || (useSSL == null && uri.toString().contains("btc-electrum"))) {
socket = await Socket.connect(host, port, timeout: connectionTimeout);
} else {
socket = await SecureSocket.connect(
host,
port,
timeout: connectionTimeout,
onBadCertificate: (_) => true,
);
}
} catch (e) {
if (e is HandshakeException) {
useSSL = !(useSSL ?? false);
}
if (_connectionStatus != ConnectionStatus.connecting) {
_setConnectionStatus(ConnectionStatus.failed);
}
return;
}
if (socket == null) {
if (_connectionStatus != ConnectionStatus.connecting) {
_setConnectionStatus(ConnectionStatus.failed);
}
return;
}
// use ping to determine actual connection status since we could've just not timed out yet:
// _setConnectionStatus(ConnectionStatus.connected);
socket!.listen(
(Uint8List event) {
try {
final msg = utf8.decode(event.toList());
final messagesList = msg.split("\n");
for (var message in messagesList) {
if (message.isEmpty) {
continue;
}
_parseResponse(message);
}
} catch (e) {
printV("socket.listen: $e");
}
},
onError: (Object error) {
final errorMsg = error.toString();
printV(errorMsg);
unterminatedString = '';
socket = null;
},
onDone: () {
printV("SOCKET CLOSED!!!!!");
unterminatedString = '';
try {
if (host == socket?.address.host || socket == null) {
_setConnectionStatus(ConnectionStatus.disconnected);
socket?.destroy();
socket = null;
}
} catch (e) {
printV("onDone: $e");
}
},
cancelOnError: true,
);
keepAlive();
}
void _parseResponse(String message) {
try {
final response = json.decode(message) as Map<String, dynamic>;
_handleResponse(response);
} on FormatException catch (e) {
final msg = e.message.toLowerCase();
if (e.source is String) {
unterminatedString += e.source as String;
}
if (msg.contains("not a subtype of type")) {
unterminatedString += e.source as String;
return;
}
if (isJSONStringCorrect(unterminatedString)) {
final response = json.decode(unterminatedString) as Map<String, dynamic>;
_handleResponse(response);
unterminatedString = '';
}
} on TypeError catch (e) {
if (!e.toString().contains('Map<String, Object>') &&
!e.toString().contains('Map<String, dynamic>')) {
return;
}
unterminatedString += message;
if (isJSONStringCorrect(unterminatedString)) {
final response = json.decode(unterminatedString) as Map<String, dynamic>;
_handleResponse(response);
// unterminatedString = null;
unterminatedString = '';
}
} catch (e) {
printV("parse $e");
}
}
void keepAlive() {
_aliveTimer?.cancel();
_aliveTimer = Timer.periodic(aliveTimerDuration, (_) async => ping());
}
Future<void> ping() async {
try {
await callWithTimeout(method: 'server.ping');
_setConnectionStatus(ConnectionStatus.connected);
} catch (_) {
_setConnectionStatus(ConnectionStatus.disconnected);
}
}
Future<List<String>> version() =>
call(method: 'server.version', params: ["", "1.4"]).then((dynamic result) {
if (result is List) {
return result.map((dynamic val) => val.toString()).toList();
}
return [];
});
Future<Map<String, dynamic>> getBalance(String scriptHash) =>
call(method: 'blockchain.scripthash.get_balance', params: [scriptHash])
.then((dynamic result) {
if (result is Map<String, dynamic>) {
return result;
}
return <String, dynamic>{};
});
Future<List<Map<String, dynamic>>> getHistory(String scriptHash) =>
call(method: 'blockchain.scripthash.get_history', params: [scriptHash])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, dynamic>) {
return val;
}
return <String, dynamic>{};
}).toList();
}
return [];
});
Future<List<Map<String, dynamic>>> getListUnspent(String scriptHash) async {
final result = await call(method: 'blockchain.scripthash.listunspent', params: [scriptHash]);
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, dynamic>) {
return val;
}
return <String, dynamic>{};
}).toList();
}
return [];
}
Future<List<Map<String, dynamic>>> getMempool(String scriptHash) =>
call(method: 'blockchain.scripthash.get_mempool', params: [scriptHash])
.then((dynamic result) {
if (result is List) {
return result.map((dynamic val) {
if (val is Map<String, dynamic>) {
return val;
}
return <String, dynamic>{};
}).toList();
}
return [];
});
Future<dynamic> getTransaction({required String hash, required bool verbose}) async {
try {
final result = await callWithTimeout(
method: 'blockchain.transaction.get', params: [hash, verbose], timeout: 10000);
return result;
} on RequestFailedTimeoutException catch (_) {
return <String, dynamic>{};
} catch (e) {
return <String, dynamic>{};
}
}
Future<Map<String, dynamic>> getTransactionVerbose({required String hash}) =>
getTransaction(hash: hash, verbose: true).then((dynamic result) {
if (result is Map<String, dynamic>) {
return result;
}
return <String, dynamic>{};
});
Future<String> getTransactionHex({required String hash}) =>
getTransaction(hash: hash, verbose: false).then((dynamic result) {
if (result is String) {
return result;
}
return '';
});
Future<String> broadcastTransaction(
{required String transactionRaw,
BasedUtxoNetwork? network,
Function(int)? idCallback}) async =>
call(
method: 'blockchain.transaction.broadcast',
params: [transactionRaw],
idCallback: idCallback)
.then((dynamic result) {
if (result is String) {
return result;
}
return '';
});
Future<Map<String, dynamic>> getMerkle({required String hash, required int height}) async =>
await call(method: 'blockchain.transaction.get_merkle', params: [hash, height])
as Map<String, dynamic>;
Future<Map<String, dynamic>> getHeader({required int height}) async =>
await call(method: 'blockchain.block.get_header', params: [height]) as Map<String, dynamic>;
BehaviorSubject<Object>? tweaksSubscribe({required int height, required int count}) =>
subscribe<Object>(
id: 'blockchain.tweaks.subscribe',
method: 'blockchain.tweaks.subscribe',
params: [height, count, true],
);
Future<dynamic> tweaksRegister({
required String secViewKey,
required String pubSpendKey,
List<int> labels = const [],
}) =>
call(
method: 'blockchain.tweaks.register',
params: [secViewKey, pubSpendKey, labels],
);
Future<dynamic> tweaksErase({required String pubSpendKey}) => call(
method: 'blockchain.tweaks.erase',
params: [pubSpendKey],
);
BehaviorSubject<Object>? tweaksScan({required String pubSpendKey}) => subscribe<Object>(
id: 'blockchain.tweaks.scan',
method: 'blockchain.tweaks.scan',
params: [pubSpendKey],
);
Future<dynamic> tweaksGet({required String pubSpendKey}) => call(
method: 'blockchain.tweaks.get',
params: [pubSpendKey],
);
Future<dynamic> getTweaks({required int height}) async =>
await callWithTimeout(method: 'blockchain.tweaks.subscribe', params: [height, 1, false]);
Future<double> estimatefee({required int p}) =>
call(method: 'blockchain.estimatefee', params: [p]).then((dynamic result) {
if (result is double) {
return result;
}
if (result is String) {
return double.parse(result);
}
return 0;
});
Future<List<List<int>>> feeHistogram() =>
call(method: 'mempool.get_fee_histogram').then((dynamic result) {
if (result is List) {
// return result.map((dynamic e) {
// if (e is List) {
// return e.map((dynamic ee) => ee is int ? ee : null).toList();
// }
// return null;
// }).toList();
final histogram = <List<int>>[];
for (final e in result) {
if (e is List) {
final eee = <int>[];
for (final ee in e) {
if (ee is int) {
eee.add(ee);
}
}
histogram.add(eee);
}
}
return histogram;
}
return [];
});
// Future<List<int>> feeRates({BasedUtxoNetwork? network}) async {
// try {
// final topDoubleString = await estimatefee(p: 1);
// final middleDoubleString = await estimatefee(p: 5);
// final bottomDoubleString = await estimatefee(p: 10);
// final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round();
// final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round();
// final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round();
// return [bottom, middle, top];
// } catch (_) {
// return [];
// }
// }
// https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-headers-subscribe
// example response:
// {
// "height": 520481,
// "hex": "00000020890208a0ae3a3892aa047c5468725846577cfcd9b512b50000000000000000005dc2b02f2d297a9064ee103036c14d678f9afc7e3d9409cf53fd58b82e938e8ecbeca05a2d2103188ce804c4"
// }
Future<int?> getCurrentBlockChainTip() async {
try {
final result = await callWithTimeout(method: 'blockchain.headers.subscribe');
if (result is Map<String, dynamic>) {
return result["height"] as int;
}
return null;
} on RequestFailedTimeoutException catch (_) {
return null;
} catch (e) {
printV("getCurrentBlockChainTip: ${e.toString()}");
return null;
}
}
BehaviorSubject<Object>? chainTipSubscribe() {
_id += 1;
return subscribe<Object>(
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
}
BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
_id += 1;
return subscribe<Object>(
id: 'blockchain.scripthash.subscribe:$scripthash',
method: 'blockchain.scripthash.subscribe',
params: [scripthash]);
}
BehaviorSubject<T>? subscribe<T>(
{required String id, required String method, List<Object> params = const []}) {
try {
if (socket == null) {
return null;
}
final subscription = BehaviorSubject<T>();
_regisrySubscription(id, subscription);
socket!.write(jsonrpc(method: method, id: _id, params: params));
return subscription;
} catch (e) {
printV("subscribe $e");
return null;
}
}
Future<dynamic> call(
{required String method, List<Object> params = const [], Function(int)? idCallback}) async {
if (socket == null) {
return null;
}
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
idCallback?.call(id);
_registryTask(id, completer);
socket!.write(jsonrpc(method: method, id: id, params: params));
return completer.future;
}
Future<dynamic> callWithTimeout(
{required String method, List<Object> params = const [], int timeout = 5000}) async {
try {
if (socket == null) {
return null;
}
final completer = Completer<dynamic>();
_id += 1;
final id = _id;
_registryTask(id, completer);
socket!.write(jsonrpc(method: method, id: id, params: params));
Timer(Duration(milliseconds: timeout), () {
if (!completer.isCompleted) {
completer.completeError(RequestFailedTimeoutException(method, id));
}
});
return completer.future;
} catch (e) {
printV("callWithTimeout $e");
rethrow;
}
}
Future<void> close() async {
_aliveTimer?.cancel();
try {
await socket?.close();
socket = null;
} catch (_) {}
onConnectionStatusChange = null;
}
void _registryTask(int id, Completer<dynamic> completer) =>
_tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false);
void _regisrySubscription(String id, BehaviorSubject<dynamic> subject) =>
_tasks[id] = SocketTask(subject: subject, isSubscription: true);
void _finish(String id, Object? data) {
if (_tasks[id] == null) {
return;
}
if (!(_tasks[id]?.completer?.isCompleted ?? false)) {
_tasks[id]?.completer!.complete(data);
}
if (!(_tasks[id]?.isSubscription ?? false)) {
_tasks.remove(id);
} else {
_tasks[id]?.subject?.add(data);
}
}
void _methodHandler({required String method, required Map<String, dynamic> request}) {
switch (method) {
case 'blockchain.headers.subscribe':
final params = request['params'] as List<dynamic>;
final id = 'blockchain.headers.subscribe';
_tasks[id]?.subject?.add(params.last);
break;
case 'blockchain.scripthash.subscribe':
final params = request['params'] as List<dynamic>;
final scripthash = params.first as String?;
final id = 'blockchain.scripthash.subscribe:$scripthash';
_tasks[id]?.subject?.add(params.last);
break;
case 'blockchain.headers.subscribe':
final params = request['params'] as List<dynamic>;
_tasks[method]?.subject?.add(params.last);
break;
case 'blockchain.tweaks.subscribe':
case 'blockchain.tweaks.scan':
final params = request['params'] as List<dynamic>;
_tasks[_tasks.keys.first]?.subject?.add(params.last);
break;
default:
break;
}
}
void _setConnectionStatus(ConnectionStatus status) {
onConnectionStatusChange?.call(status);
_connectionStatus = status;
_isConnected = status == ConnectionStatus.connected;
if (!_isConnected) {
try {
socket?.destroy();
} catch (_) {}
socket = null;
}
}
void _handleResponse(Map<String, dynamic> response) {
final method = response['method'];
final id = response['id'] as String?;
final result = response['result'];
try {
final error = response['error'] as Map<String, dynamic>?;
if (error != null) {
final errorMessage = error['message'] as String?;
if (errorMessage != null) {
_errors[id!] = errorMessage;
}
}
} catch (_) {}
try {
final error = response['error'] as String?;
if (error != null) {
_errors[id!] = error;
}
} catch (_) {}
if (method is String) {
_methodHandler(method: method, request: response);
return;
}
if (id != null) {
_finish(id, result);
}
}
String getErrorMessage(int id) => _errors[id.toString()] ?? '';
}
// FIXME: move me
bool isJSONStringCorrect(String source) {
try {
json.decode(source);
return true;
} catch (_) {
return false;
}
}
class RequestFailedTimeoutException implements Exception {
RequestFailedTimeoutException(this.method, this.id);
final String method;
final int id;
}

View file

@ -62,7 +62,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
required bool isPending,
bool isReplaced = false,
required DateTime date,
required int? time,
int? time,
bool? isDateValidated,
required int confirmations,
String? to,

View file

@ -417,7 +417,6 @@ abstract class ElectrumWalletBase
_workerIsolate = await Isolate.spawn<SendPort>(ElectrumWorker.run, receivePort!.sendPort);
_workerSubscription = receivePort!.listen((message) {
printV('Main: received message: $message');
if (message is SendPort) {
workerSendPort = message;
workerSendPort!.send(
@ -439,13 +438,12 @@ abstract class ElectrumWalletBase
}
}
int get _dustAmount => 546;
int get _dustAmount => 0;
bool _isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
bool isBelowDust(int amount) => amount <= _dustAmount && network != BitcoinNetwork.testnet;
TxCreateUtxoDetails _createUTXOS({
TxCreateUtxoDetails createUTXOS({
required bool sendAll,
required bool paysToSilentPayment,
int credentialsAmount = 0,
int? inputsCount,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
@ -455,7 +453,6 @@ abstract class ElectrumWalletBase
List<ECPrivateInfo> inputPrivKeyInfos = [];
final publicKeys = <String, PublicKeyWithDerivationPath>{};
int allInputsAmount = 0;
bool spendsSilentPayment = false;
bool spendsUnconfirmedTX = false;
int leftAmount = credentialsAmount;
@ -483,25 +480,13 @@ abstract class ElectrumWalletBase
final utx = availableInputs[i];
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
if (paysToSilentPayment) {
// Check inputs for shared secret derivation
if (utx.bitcoinAddressRecord.addressType == SegwitAddresType.p2wsh) {
throw BitcoinTransactionSilentPaymentsNotSupported();
}
}
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = RegexUtils.addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
bool? isSilentPayment = false;
if (utx.bitcoinAddressRecord is BitcoinReceivedSPAddressRecord) {
privkey = (utx.bitcoinAddressRecord as BitcoinReceivedSPAddressRecord).spendKey;
spendsSilentPayment = true;
isSilentPayment = true;
} else if (!isHardwareWallet) {
if (!isHardwareWallet) {
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
final path = addressRecord.derivationInfo.derivationPath
.addElem(Bip32KeyIndex(
@ -516,11 +501,7 @@ abstract class ElectrumWalletBase
String pubKeyHex;
if (privkey != null) {
inputPrivKeyInfos.add(ECPrivateInfo(
privkey,
address.type == SegwitAddresType.p2tr,
tweak: !isSilentPayment,
));
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
pubKeyHex = privkey.getPublic().toHex();
} else {
@ -545,7 +526,6 @@ abstract class ElectrumWalletBase
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: BitcoinAddressUtils.getScriptType(address),
isSilentPayment: isSilentPayment,
),
ownerDetails: UtxoAddressDetails(
publicKey: pubKeyHex,
@ -575,7 +555,6 @@ abstract class ElectrumWalletBase
inputPrivKeyInfos: inputPrivKeyInfos,
publicKeys: publicKeys,
allInputsAmount: allInputsAmount,
spendsSilentPayment: spendsSilentPayment,
spendsUnconfirmedTX: spendsUnconfirmedTX,
);
}
@ -584,14 +563,9 @@ abstract class ElectrumWalletBase
List<BitcoinOutput> outputs,
int feeRate, {
String? memo,
bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
final utxoDetails = _createUTXOS(
sendAll: true,
paysToSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom);
int fee = await calcFee(
utxos: utxoDetails.utxos,
@ -612,7 +586,7 @@ abstract class ElectrumWalletBase
}
// Attempting to send less than the dust limit
if (_isBelowDust(amount)) {
if (isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
}
@ -630,31 +604,27 @@ abstract class ElectrumWalletBase
hasChange: false,
memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
spendsSilentPayment: utxoDetails.spendsSilentPayment,
);
}
Future<EstimatedTxResult> estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
List<BitcoinOutput> 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)) {
if (isBelowDust(credentialsAmount)) {
throw BitcoinTransactionNoDustException();
}
final utxoDetails = _createUTXOS(
final utxoDetails = createUTXOS(
sendAll: false,
credentialsAmount: credentialsAmount,
inputsCount: inputsCount,
paysToSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
@ -671,11 +641,9 @@ abstract class ElectrumWalletBase
return estimateTxForAmount(
credentialsAmount,
outputs,
updatedOutputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
@ -685,14 +653,9 @@ abstract class ElectrumWalletBase
final changeAddress = await walletAddresses.getChangeAddress(
inputs: utxoDetails.availableInputs,
outputs: updatedOutputs,
outputs: outputs,
);
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),
@ -703,34 +666,25 @@ abstract class ElectrumWalletBase
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,
outputs: outputs,
memo: memo,
feeRate: feeRate,
);
updatedOutputs.clear();
updatedOutputs.addAll(temp);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
}
int amount = credentialsAmount;
final lastOutput = updatedOutputs.last;
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (_isBelowDust(amountLeftForChange)) {
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();
outputs.removeLast();
if (amountLeftForChange < 0) {
@ -738,12 +692,10 @@ abstract class ElectrumWalletBase
return estimateTxForAmount(
credentialsAmount,
outputs,
updatedOutputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
} else {
@ -761,20 +713,12 @@ abstract class ElectrumWalletBase
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,
);
@ -788,7 +732,6 @@ abstract class ElectrumWalletBase
isSendAll: spendingAllCoins,
memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
spendsSilentPayment: utxoDetails.spendsSilentPayment,
);
}
}
@ -818,12 +761,11 @@ abstract class ElectrumWalletBase
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
int credentialsAmount = 0;
bool hasSilentPayment = false;
for (final out in transactionCredentials.outputs) {
final outputAmount = out.formattedCryptoAmount!;
if (!sendAll && _isBelowDust(outputAmount)) {
if (!sendAll && isBelowDust(outputAmount)) {
throw BitcoinTransactionNoDustException();
}
@ -836,25 +778,20 @@ abstract class ElectrumWalletBase
credentialsAmount += outputAmount;
final address = RegexUtils.addressTypeFromStr(
out.isParsedAddress ? out.extractedAddress! : out.address, network);
final isSilentPayment = address is SilentPaymentAddress;
if (isSilentPayment) {
hasSilentPayment = true;
}
out.isParsedAddress ? out.extractedAddress! : out.address,
network,
);
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,
));
}
}
@ -864,31 +801,19 @@ abstract class ElectrumWalletBase
: 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,
outputs,
feeRateInt,
memo: memo,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
} else {
estimatedTx = await estimateTxForAmount(
credentialsAmount,
outputs,
updatedOutputs,
feeRateInt,
memo: memo,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
@ -896,7 +821,7 @@ abstract class ElectrumWalletBase
if (walletInfo.isHardwareWallet) {
final transaction = await buildHardwareWalletTransaction(
utxos: estimatedTx.utxos,
outputs: updatedOutputs,
outputs: outputs,
publicKeys: estimatedTx.publicKeys,
fee: BigInt.from(estimatedTx.fee),
network: network,
@ -925,7 +850,7 @@ abstract class ElectrumWalletBase
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: updatedOutputs,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
@ -935,7 +860,7 @@ abstract class ElectrumWalletBase
} else {
txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: updatedOutputs,
outputs: outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
@ -974,11 +899,7 @@ abstract class ElectrumWalletBase
if (utxo.utxo.isP2tr()) {
hasTaprootInputs = true;
return key.privkey.signTapRoot(
txDigest,
sighash: sighash,
tweak: utxo.utxo.isSilentPayment != true,
);
return key.privkey.signTapRoot(txDigest, sighash: sighash);
} else {
return key.privkey.signInput(txDigest, sigHash: sighash);
}
@ -997,20 +918,14 @@ abstract class ElectrumWalletBase
utxos: estimatedTx.utxos,
)..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();
});
} catch (e) {
} catch (e, s) {
print([e, s]);
throw e;
}
}
@ -1974,7 +1889,7 @@ class EstimatedTxResult {
required this.hasChange,
required this.isSendAll,
this.memo,
required this.spendsSilentPayment,
this.spendsSilentPayment = false,
required this.spendsUnconfirmedTX,
});
@ -1985,7 +1900,6 @@ class EstimatedTxResult {
final int amount;
final bool spendsSilentPayment;
// final bool sendsToSilentPayment;
final bool hasChange;
final bool isSendAll;
final String? memo;
@ -2018,7 +1932,7 @@ class TxCreateUtxoDetails {
required this.inputPrivKeyInfos,
required this.publicKeys,
required this.allInputsAmount,
required this.spendsSilentPayment,
this.spendsSilentPayment = false,
required this.spendsUnconfirmedTX,
});
}

View file

@ -382,12 +382,21 @@ class ElectrumWorker {
}
Future<void> _handleBroadcast(ElectrumWorkerBroadcastRequest request) async {
final rpcId = _electrumClient!.id + 1;
final txHash = await _electrumClient!.request(
ElectrumBroadCastTransaction(transactionRaw: request.transactionRaw),
);
if (txHash == null) {
final error = (_electrumClient!.rpc as ElectrumSSLService).getError(rpcId);
if (error?.message != null) {
return _sendError(ElectrumWorkerBroadcastError(error: error!.message, id: request.id));
}
} else {
_sendResponse(ElectrumWorkerBroadcastResponse(txHash: txHash, id: request.id));
}
}
Future<void> _handleGetTxExpanded(ElectrumWorkerTxExpandedRequest request) async {
final tx = await _getTransactionExpanded(
@ -586,6 +595,7 @@ class ElectrumWorker {
if (scanData.shouldSwitchNodes) {
scanningClient = await ElectrumApiProvider.connect(
ElectrumTCPService.connect(
// TODO: ssl
Uri.parse("tcp://electrs.cakewallet.com:50001"),
),
);
@ -714,7 +724,6 @@ class ElectrumWorker {
date: scanData.network == BitcoinNetwork.mainnet
? getDateByBitcoinHeight(tweakHeight)
: DateTime.now(),
time: null,
confirmations: scanData.chainTip - tweakHeight + 1,
unspents: [],
isReceivedSilentPayment: true,
@ -736,10 +745,7 @@ class ElectrumWorker {
receivingOutputAddress,
labelIndex: 1, // TODO: get actual index/label
isUsed: true,
// TODO: use right wallet
spendKey: scanData.silentPaymentsWallets.first.b_spend.tweakAdd(
BigintUtils.fromBytes(BytesUtils.fromHexString(t_k)),
),
tweak: t_k,
txCount: 1,
balance: amount,
);

View file

@ -559,7 +559,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
direction: TransactionDirection.incoming,
isPending: utxo.height == 0,
date: date,
time: null,
confirmations: confirmations,
inputAddresses: [],
outputAddresses: [utxo.outputId],
@ -763,7 +762,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
direction: TransactionDirection.outgoing,
isPending: false,
date: DateTime.fromMillisecondsSinceEpoch(status.blockTime * 1000),
time: null,
confirmations: 1,
inputAddresses: inputAddresses.toList(),
outputAddresses: [],

View file

@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:grpc/grpc.dart';
@ -24,6 +26,7 @@ class PendingBitcoinTransaction with PendingTransaction {
this.hasTaprootInputs = false,
this.isMweb = false,
this.utxos = const [],
this.hasSilentPayment = false,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
@ -59,7 +62,7 @@ class PendingBitcoinTransaction with PendingTransaction {
List<TxOutput> get outputs => _tx.outputs;
bool get hasSilentPayment => _tx.hasSilentPayment;
bool hasSilentPayment;
PendingChange? get change {
try {
@ -76,41 +79,41 @@ class PendingBitcoinTransaction with PendingTransaction {
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
Future<void> _commit() async {
int? callId;
final result = await sendWorker(
ElectrumWorkerBroadcastRequest(transactionRaw: hex),
) as String;
final result = await sendWorker(ElectrumWorkerBroadcastRequest(transactionRaw: hex)) as String;
String? error;
try {
final resultJson = jsonDecode(result) as Map<String, dynamic>;
error = resultJson["error"] as String;
} catch (_) {}
// if (result.isEmpty) {
// if (callId != null) {
// final error = sendWorker(getErrorMessage(callId!));
if (error != null) {
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
} else if (!isSendAll) {
throw BitcoinTransactionCommitFailedDustOutput();
} else {
throw BitcoinTransactionCommitFailedDustOutputSendAll();
}
}
// if (error.contains("dust")) {
// if (hasChange) {
// throw BitcoinTransactionCommitFailedDustChange();
// } else if (!isSendAll) {
// throw BitcoinTransactionCommitFailedDustOutput();
// } else {
// throw BitcoinTransactionCommitFailedDustOutputSendAll();
// }
// }
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
// if (error.contains("bad-txns-vout-negative")) {
// throw BitcoinTransactionCommitFailedVoutNegative();
// }
if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final();
}
// if (error.contains("non-BIP68-final")) {
// throw BitcoinTransactionCommitFailedBIP68Final();
// }
if (error.contains("min fee not met")) {
throw BitcoinTransactionCommitFailedLessThanMin();
}
// if (error.contains("min fee not met")) {
// throw BitcoinTransactionCommitFailedLessThanMin();
// }
// throw BitcoinTransactionCommitFailed(errorMessage: error);
// }
// throw BitcoinTransactionCommitFailed();
// }
throw BitcoinTransactionCommitFailed(errorMessage: error);
}
}
Future<void> _ltcCommit() async {
@ -151,7 +154,6 @@ class PendingBitcoinTransaction with PendingTransaction {
inputAddresses: _tx.inputs.map((input) => input.txId).toList(),
outputAddresses: outputAddresses,
fee: fee,
time: null,
);
@override

View file

@ -1,24 +1,29 @@
import 'dart:convert';
import 'package:cw_bitcoin/electrum_worker/electrum_worker_params.dart';
import 'package:cw_bitcoin/electrum_worker/methods/methods.dart';
import 'package:cw_bitcoin/exceptions.dart';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
class PendingBitcoinCashTransaction with PendingTransaction {
PendingBitcoinCashTransaction(this._tx, this.type,
{required this.electrumClient,
PendingBitcoinCashTransaction(
this._tx,
this.type, {
required this.sendWorker,
required this.amount,
required this.fee,
required this.hasChange,
required this.isSendAll})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
required this.isSendAll,
}) : _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
final bitbox.Transaction _tx;
final ElectrumClient electrumClient;
Future<dynamic> Function(ElectrumWorkerRequest) sendWorker;
final int amount;
final int fee;
final bool hasChange;
@ -40,15 +45,17 @@ class PendingBitcoinCashTransaction with PendingTransaction {
@override
Future<void> commit() async {
int? callId;
final result = await sendWorker(
ElectrumWorkerBroadcastRequest(transactionRaw: hex),
) as String;
final result = await electrumClient.broadcastTransaction(
transactionRaw: hex, idCallback: (id) => callId = id);
if (result.isEmpty) {
if (callId != null) {
final error = electrumClient.getErrorMessage(callId!);
String? error;
try {
final resultJson = jsonDecode(result) as Map<String, dynamic>;
error = resultJson["error"] as String;
} catch (_) {}
if (error != null) {
if (error.contains("dust")) {
if (hasChange) {
throw BitcoinTransactionCommitFailedDustChange();
@ -62,10 +69,16 @@ class PendingBitcoinCashTransaction with PendingTransaction {
if (error.contains("bad-txns-vout-negative")) {
throw BitcoinTransactionCommitFailedVoutNegative();
}
throw BitcoinTransactionCommitFailed(errorMessage: error);
if (error.contains("non-BIP68-final")) {
throw BitcoinTransactionCommitFailedBIP68Final();
}
throw BitcoinTransactionCommitFailed();
if (error.contains("min fee not met")) {
throw BitcoinTransactionCommitFailedLessThanMin();
}
throw BitcoinTransactionCommitFailed(errorMessage: error);
}
_listeners.forEach((listener) => listener(transactionInfo()));
@ -86,6 +99,7 @@ class PendingBitcoinCashTransaction with PendingTransaction {
fee: fee,
isReplaced: false,
);
@override
Future<String?> commitUR() {
throw UnimplementedError();

View file

@ -695,7 +695,7 @@
"sent": "Enviada",
"service_health_disabled": "O Boletim de Saúde de Serviço está desativado",
"service_health_disabled_message": "Esta é a página do Boletim de Saúde de Serviço, você pode ativar esta página em Configurações -> Privacidade",
"set_a_pin": "Defina um pino",
"set_a_pin": "Defina um pin",
"settings": "Configurações",
"settings_all": "Tudo",
"settings_allow_biometrical_authentication": "Permitir autenticação biométrica",