Skip to content

Commit

Permalink
Merge branch 'main' into CW-782-show-error-report-popup-without-cooldown
Browse files Browse the repository at this point in the history
  • Loading branch information
OmarHatem28 authored Nov 28, 2024
2 parents 9fd5b26 + 9cd69c4 commit d0d398c
Show file tree
Hide file tree
Showing 66 changed files with 1,162 additions and 439 deletions.
9 changes: 9 additions & 0 deletions cw_bitcoin/lib/bitcoin_wallet_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ class BitcoinWalletService extends WalletService<
final walletInfo = walletInfoSource.values
.firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!;
await walletInfoSource.delete(walletInfo.key);

final unspentCoinsToDelete = unspentCoinsInfoSource.values.where(
(unspentCoin) => unspentCoin.walletId == walletInfo.id).toList();

final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList();

if (keysToDelete.isNotEmpty) {
await unspentCoinsInfoSource.deleteAll(keysToDelete);
}
}

@override
Expand Down
206 changes: 157 additions & 49 deletions cw_bitcoin/lib/electrum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ abstract class ElectrumWalletBase
Future<void> init() async {
await walletAddresses.init();
await transactionHistory.init();
await cleanUpDuplicateUnspentCoins();
await save();

_autoSaveTimer =
Expand Down Expand Up @@ -1379,10 +1380,11 @@ abstract class ElectrumWalletBase
}));

unspentCoins = updatedUnspentCoins;

final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id);

if (unspentCoinsInfo.length != updatedUnspentCoins.length) {
if (currentWalletUnspentCoins.length != updatedUnspentCoins.length) {
unspentCoins.forEach((coin) => addCoinInfo(coin));
return;
}

await updateCoins(unspentCoins);
Expand All @@ -1408,6 +1410,7 @@ abstract class ElectrumWalletBase
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;

if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
Expand Down Expand Up @@ -1445,20 +1448,27 @@ abstract class ElectrumWalletBase

@action
Future<void> addCoinInfo(BitcoinUnspent coin) async {
final newInfo = UnspentCoinsInfo(
walletId: id,
hash: coin.hash,
isFrozen: coin.isFrozen,
isSending: coin.isSending,
noteRaw: coin.note,
address: coin.bitcoinAddressRecord.address,
value: coin.value,
vout: coin.vout,
isChange: coin.isChange,
isSilentPayment: coin is BitcoinSilentPaymentsUnspent,
);

await unspentCoinsInfo.add(newInfo);
// Check if the coin is already in the unspentCoinsInfo for the wallet
final existingCoinInfo = unspentCoinsInfo.values.firstWhereOrNull(
(element) => element.walletId == walletInfo.id && element == coin);

if (existingCoinInfo == null) {
final newInfo = UnspentCoinsInfo(
walletId: id,
hash: coin.hash,
isFrozen: coin.isFrozen,
isSending: coin.isSending,
noteRaw: coin.note,
address: coin.bitcoinAddressRecord.address,
value: coin.value,
vout: coin.vout,
isChange: coin.isChange,
isSilentPayment: coin is BitcoinSilentPaymentsUnspent,
);

await unspentCoinsInfo.add(newInfo);
}
}

Future<void> _refreshUnspentCoinsInfo() async {
Expand Down Expand Up @@ -1486,6 +1496,23 @@ abstract class ElectrumWalletBase
}
}

Future<void> cleanUpDuplicateUnspentCoins() async {
final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) => element.walletId == id);
final Map<String, UnspentCoinsInfo> uniqueUnspentCoins = {};
final List<dynamic> duplicateKeys = [];

for (final unspentCoin in currentWalletUnspentCoins) {
final key = '${unspentCoin.hash}:${unspentCoin.vout}';
if (!uniqueUnspentCoins.containsKey(key)) {
uniqueUnspentCoins[key] = unspentCoin;
} else {
duplicateKeys.add(unspentCoin.key);
}
}

if (duplicateKeys.isNotEmpty) await unspentCoinsInfo.deleteAll(duplicateKeys);
}

int transactionVSize(String transactionHex) => BtcTransaction.fromRaw(transactionHex).getVSize();

Future<String?> canReplaceByFee(ElectrumTransactionInfo tx) async {
Expand All @@ -1503,14 +1530,21 @@ abstract class ElectrumWalletBase
final bundle = await getTransactionExpanded(hash: txId);
final outputs = bundle.originalTransaction.outputs;

final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
final ownAddresses = walletAddresses.allAddresses.map((addr) => addr.address).toSet();

final receiverAmount = outputs
.where((output) => !ownAddresses.contains(addressFromOutputScript(output.scriptPubKey, network)))
.fold<int>(0, (sum, output) => sum + output.amount.toInt());

// look for a change address in the outputs
final changeOutput = outputs.firstWhereOrNull((output) => changeAddresses.any(
(element) => element.address == addressFromOutputScript(output.scriptPubKey, network)));
if (receiverAmount == 0) {
throw Exception("Receiver output not found.");
}

var allInputsAmount = 0;
final availableInputs = unspentCoins.where((utxo) => utxo.isSending && !utxo.isFrozen).toList();
int totalBalance = availableInputs.fold<int>(
0, (previousValue, element) => previousValue + element.value.toInt());

int allInputsAmount = 0;
for (int i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
Expand All @@ -1521,25 +1555,24 @@ abstract class ElectrumWalletBase

int totalOutAmount = bundle.originalTransaction.outputs
.fold<int>(0, (previousValue, element) => previousValue + element.amount.toInt());

var currentFee = allInputsAmount - totalOutAmount;

int remainingFee = (newFee - currentFee > 0) ? newFee - currentFee : newFee;

return changeOutput != null && changeOutput.amount.toInt() - remainingFee >= 0;
return totalBalance - receiverAmount - remainingFee >= _dustAmount;
}

Future<PendingBitcoinTransaction> replaceByFee(String hash, int newFee) async {
try {
final bundle = await getTransactionExpanded(hash: hash);

final utxos = <UtxoWithAddress>[];
final outputs = <BitcoinOutput>[];
List<ECPrivate> privateKeys = [];

var allInputsAmount = 0;
String? memo;

// Add inputs
// Add original inputs
for (var i = 0; i < bundle.originalTransaction.inputs.length; i++) {
final input = bundle.originalTransaction.inputs[i];
final inputTransaction = bundle.ins[i];
Expand All @@ -1549,8 +1582,7 @@ abstract class ElectrumWalletBase
allInputsAmount += outTransaction.amount.toInt();

final addressRecord =
walletAddresses.allAddresses.firstWhere((element) => element.address == address);

walletAddresses.allAddresses.firstWhere((element) => element.address == address);
final btcAddress = RegexUtils.addressTypeFromStr(addressRecord.address, network);
final privkey = generateECPrivate(
hd: addressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
Expand All @@ -1568,15 +1600,13 @@ abstract class ElectrumWalletBase
scriptType: _getScriptType(btcAddress),
),
ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: btcAddress),
),
);
}

// Create a list of available outputs
final outputs = <BitcoinOutput>[];
// Add original outputs
for (final out in bundle.originalTransaction.outputs) {
// Check if the script contains OP_RETURN
final script = out.scriptPubKey.script;
if (script.contains('OP_RETURN') && memo == null) {
final index = script.indexOf('OP_RETURN');
Expand All @@ -1598,25 +1628,103 @@ abstract class ElectrumWalletBase

// Calculate the total amount and fees
int totalOutAmount =
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
outputs.fold<int>(0, (previousValue, output) => previousValue + output.value.toInt());
int currentFee = allInputsAmount - totalOutAmount;
int remainingFee = newFee - currentFee;

if (remainingFee <= 0) {
throw Exception("New fee must be higher than the current fee.");
}

// Deduct Remaining Fee from Main Outputs
// Deduct fee from change outputs first, if possible
if (remainingFee > 0) {
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
for (int i = outputs.length - 1; i >= 0; i--) {
int outputAmount = outputs[i].value.toInt();
final output = outputs[i];
final isChange = changeAddresses
.any((element) => element.address == output.address.toAddress(network));

if (isChange) {
int outputAmount = output.value.toInt();
if (outputAmount > _dustAmount) {
int deduction = (outputAmount - _dustAmount >= remainingFee)
? remainingFee
: outputAmount - _dustAmount;
outputs[i] = BitcoinOutput(
address: output.address, value: BigInt.from(outputAmount - deduction));
remainingFee -= deduction;

if (remainingFee <= 0) break;
}
}
}
}

// If still not enough, add UTXOs until the fee is covered
if (remainingFee > 0) {
final unusedUtxos = unspentCoins
.where((utxo) => utxo.isSending && !utxo.isFrozen && utxo.confirmations! > 0)
.toList();

for (final utxo in unusedUtxos) {
final address = RegexUtils.addressTypeFromStr(utxo.address, network);
final privkey = generateECPrivate(
hd: utxo.bitcoinAddressRecord.isHidden
? walletAddresses.sideHd
: walletAddresses.mainHd,
index: utxo.bitcoinAddressRecord.index,
network: network,
);
privateKeys.add(privkey);

utxos.add(UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utxo.hash,
value: BigInt.from(utxo.value),
vout: utxo.vout,
scriptType: _getScriptType(address)),
ownerDetails:
UtxoAddressDetails(publicKey: privkey.getPublic().toHex(), address: address),
));

allInputsAmount += utxo.value;
remainingFee -= utxo.value;

if (remainingFee < 0) {
final changeOutput = outputs.firstWhereOrNull((output) => walletAddresses.allAddresses
.any((addr) => addr.address == output.address.toAddress(network)));
if (changeOutput != null) {
final newValue = changeOutput.value.toInt() + (-remainingFee);
outputs[outputs.indexOf(changeOutput)] =
BitcoinOutput(address: changeOutput.address, value: BigInt.from(newValue));
} else {
final changeAddress = await walletAddresses.getChangeAddress();
outputs.add(BitcoinOutput(
address: RegexUtils.addressTypeFromStr(changeAddress.address, network),
value: BigInt.from(-remainingFee)));
}

remainingFee = 0;
break;
}

if (remainingFee <= 0) break;
}
}

// Deduct from the receiver's output if remaining fee is still greater than 0
if (remainingFee > 0) {
for (int i = 0; i < outputs.length; i++) {
final output = outputs[i];
int outputAmount = output.value.toInt();

if (outputAmount > _dustAmount) {
int deduction = (outputAmount - _dustAmount >= remainingFee)
? remainingFee
: outputAmount - _dustAmount;

outputs[i] = BitcoinOutput(
address: outputs[i].address, value: BigInt.from(outputAmount - deduction));
address: output.address, value: BigInt.from(outputAmount - deduction));
remainingFee -= deduction;

if (remainingFee <= 0) break;
Expand All @@ -1633,11 +1741,11 @@ abstract class ElectrumWalletBase
final changeAddresses = walletAddresses.allAddresses.where((element) => element.isHidden);
final List<BitcoinOutput> changeOutputs = outputs
.where((output) => changeAddresses
.any((element) => element.address == output.address.toAddress(network)))
.any((element) => element.address == output.address.toAddress(network)))
.toList();

int totalChangeAmount =
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());
changeOutputs.fold<int>(0, (sum, output) => sum + output.value.toInt());

// The final amount that the receiver will receive
int sendingAmount = allInputsAmount - newFee - totalChangeAmount;
Expand All @@ -1654,8 +1762,7 @@ abstract class ElectrumWalletBase

final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
final key =
privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);

privateKeys.firstWhereOrNull((element) => element.getPublic().toHex() == publicKey);
if (key == null) {
throw Exception("Cannot find private key");
}
Expand All @@ -1665,6 +1772,7 @@ abstract class ElectrumWalletBase
} else {
return key.signInput(txDigest, sigHash: sighash);
}

});

return PendingBitcoinTransaction(
Expand All @@ -1677,16 +1785,16 @@ abstract class ElectrumWalletBase
hasChange: changeOutputs.isNotEmpty,
feeRate: newFee.toString(),
)..addListener((transaction) async {
transactionHistory.transactions.values.forEach((tx) {
if (tx.id == hash) {
tx.isReplaced = true;
tx.isPending = false;
transactionHistory.addOne(tx);
}
});
transactionHistory.addOne(transaction);
await updateBalance();
transactionHistory.transactions.values.forEach((tx) {
if (tx.id == hash) {
tx.isReplaced = true;
tx.isPending = false;
transactionHistory.addOne(tx);
}
});
transactionHistory.addOne(transaction);
await updateBalance();
});
} catch (e) {
throw e;
}
Expand All @@ -1707,7 +1815,7 @@ abstract class ElectrumWalletBase
try {
final blockHash = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block-height/$height",
"https://mempool.cakewallet.com/api/v1/block-height/$height",
),
);

Expand All @@ -1716,7 +1824,7 @@ abstract class ElectrumWalletBase
jsonDecode(blockHash.body) != null) {
final blockResponse = await http.get(
Uri.parse(
"http://mempool.cakewallet.com:8999/api/v1/block/${blockHash.body}",
"https://mempool.cakewallet.com/api/v1/block/${blockHash.body}",
),
);
if (blockResponse.statusCode == 200 &&
Expand Down
9 changes: 9 additions & 0 deletions cw_bitcoin/lib/litecoin_wallet_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ class LitecoinWalletService extends WalletService<
mwebdLogs.deleteSync();
}
}

final unspentCoinsToDelete = unspentCoinsInfoSource.values.where(
(unspentCoin) => unspentCoin.walletId == walletInfo.id).toList();

final keysToDelete = unspentCoinsToDelete.map((unspentCoin) => unspentCoin.key).toList();

if (keysToDelete.isNotEmpty) {
await unspentCoinsInfoSource.deleteAll(keysToDelete);
}
}

@override
Expand Down
Loading

0 comments on commit d0d398c

Please sign in to comment.