fix: some fixes for stored silent payment data, and fee usage

This commit is contained in:
Rafael Saes 2023-12-08 17:56:48 -03:00
parent ea2161010f
commit c74e335876
4 changed files with 149 additions and 50 deletions

View file

@ -8,16 +8,30 @@ class BitcoinUnspent extends Unspent {
: bitcoinAddressRecord = addressRecord, : bitcoinAddressRecord = addressRecord,
super(addressRecord.address, hash, value, vout, null); super(addressRecord.address, hash, value, vout, null);
factory BitcoinUnspent.fromJSON(BitcoinAddressRecord address, Map<String, dynamic> json) => factory BitcoinUnspent.fromJSON(BitcoinAddressRecord? address, Map<String, dynamic> json) =>
BitcoinUnspent( BitcoinUnspent(
address, address ?? BitcoinAddressRecord.fromJSON(json['address_record'] as String),
json['tx_hash'] as String, json['tx_hash'] as String,
json['value'] as int, json['value'] as int,
json['tx_pos'] as int, json['tx_pos'] as int,
silentPaymentTweak: json['silent_payment_tweak'] as String?, silentPaymentTweak: json['silent_payment_tweak'] as String?,
type: json['type'] == null ? null : AddressType.values[json['type'] as int], type: json['type'] == null
? null
: AddressType.values.firstWhere((e) => e.toString() == json['type']),
); );
Map<String, dynamic> toJson() {
final json = <String, dynamic>{
'address_record': bitcoinAddressRecord.toJSON(),
'tx_hash': hash,
'value': value,
'tx_pos': vout,
'silent_payment_tweak': silentPaymentTweak,
'type': type.toString(),
};
return json;
}
final BitcoinAddressRecord bitcoinAddressRecord; final BitcoinAddressRecord bitcoinAddressRecord;
String? silentPaymentTweak; String? silentPaymentTweak;
AddressType? type; AddressType? type;

View file

@ -3,6 +3,7 @@ import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData;
import 'package:cw_bitcoin/address_from_output.dart'; import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/format_amount.dart'; import 'package:cw_core/format_amount.dart';
@ -18,6 +19,8 @@ class ElectrumTransactionBundle {
} }
class ElectrumTransactionInfo extends TransactionInfo { class ElectrumTransactionInfo extends TransactionInfo {
BitcoinUnspent? unspent;
ElectrumTransactionInfo(this.type, ElectrumTransactionInfo(this.type,
{required String id, {required String id,
required int height, required int height,
@ -26,7 +29,9 @@ class ElectrumTransactionInfo extends TransactionInfo {
required TransactionDirection direction, required TransactionDirection direction,
required bool isPending, required bool isPending,
required DateTime date, required DateTime date,
required int confirmations}) { required int confirmations,
String? to,
this.unspent}) {
this.id = id; this.id = id;
this.height = height; this.height = height;
this.amount = amount; this.amount = amount;
@ -35,6 +40,7 @@ class ElectrumTransactionInfo extends TransactionInfo {
this.date = date; this.date = date;
this.isPending = isPending; this.isPending = isPending;
this.confirmations = confirmations; this.confirmations = confirmations;
this.to = to;
} }
factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type, factory ElectrumTransactionInfo.fromElectrumVerbose(Map<String, Object> obj, WalletType type,
@ -168,7 +174,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
} }
factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) { factory ElectrumTransactionInfo.fromJson(Map<String, dynamic> data, WalletType type) {
return ElectrumTransactionInfo(type, return ElectrumTransactionInfo(
type,
id: data['id'] as String, id: data['id'] as String,
height: data['height'] as int, height: data['height'] as int,
amount: data['amount'] as int, amount: data['amount'] as int,
@ -176,7 +183,12 @@ class ElectrumTransactionInfo extends TransactionInfo {
direction: parseTransactionDirectionFromInt(data['direction'] as int), direction: parseTransactionDirectionFromInt(data['direction'] as int),
date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int),
isPending: data['isPending'] as bool, isPending: data['isPending'] as bool,
confirmations: data['confirmations'] as int); confirmations: data['confirmations'] as int,
to: data['to'] as String?,
unspent: data['unspent'] != null
? BitcoinUnspent.fromJSON(null, data['unspent'] as Map<String, dynamic>)
: null,
);
} }
final WalletType type; final WalletType type;
@ -220,6 +232,8 @@ class ElectrumTransactionInfo extends TransactionInfo {
m['isPending'] = isPending; m['isPending'] = isPending;
m['confirmations'] = confirmations; m['confirmations'] = confirmations;
m['fee'] = fee; m['fee'] = fee;
m['to'] = to;
m['unspent'] = unspent?.toJson() ?? <String, dynamic>{};
return m; return m;
} }
} }

View file

@ -4,6 +4,8 @@ import 'dart:io';
import 'dart:isolate'; import 'dart:isolate';
import 'dart:math'; import 'dart:math';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/unspent_coins_info.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
@ -165,10 +167,7 @@ abstract class ElectrumWalletBase
startRefresh, startRefresh,
ScanData( ScanData(
sendPort: receivePort.sendPort, sendPort: receivePort.sendPort,
scanPrivkeyCompressed: primarySilentAddress: walletAddresses.primarySilentAddress!,
walletAddresses.primarySilentAddress!.scanPrivkey.toCompressedHex().fromHex,
spendPubkeyCompressed:
walletAddresses.primarySilentAddress!.spendPubkey.toCompressedHex().fromHex,
networkType: networkType, networkType: networkType,
height: height, height: height,
chainTip: currentChainTip, chainTip: currentChainTip,
@ -180,13 +179,31 @@ abstract class ElectrumWalletBase
await for (var message in receivePort) { await for (var message in receivePort) {
if (message is BitcoinUnspent) { if (message is BitcoinUnspent) {
if (!unspentCoins.any((utx) =>
utx.hash.contains(message.hash) &&
utx.vout == message.vout &&
utx.address.contains(message.address))) {
unspentCoins.add(message); unspentCoins.add(message);
await _addCoinInfo(message);
balance[currency] = await _fetchBalances(); if (unspentCoinsInfo.values.any((element) =>
element.walletId.contains(id) &&
element.hash.contains(message.hash) &&
element.address.contains(message.address))) {
_addCoinInfo(message);
await walletInfo.save(); await walletInfo.save();
await save(); await save();
} }
balance[currency] = await _fetchBalances();
}
}
if (message is Map<String, ElectrumTransactionInfo>) {
transactionHistory.addMany(message);
await transactionHistory.save();
}
// check if is a SyncStatus type since "is SyncStatus" doesn't work here // check if is a SyncStatus type since "is SyncStatus" doesn't work here
if (message is SyncResponse) { if (message is SyncResponse) {
syncStatus = message.syncStatus; syncStatus = message.syncStatus;
@ -268,7 +285,7 @@ abstract class ElectrumWalletBase
throw BitcoinTransactionNoInputsException(); throw BitcoinTransactionNoInputsException();
} }
final minAmount = networkType == bitcoin.testnet ? 0 : 546; final minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials; final transactionCredentials = credentials as BitcoinTransactionCredentials;
final outputs = transactionCredentials.outputs; final outputs = transactionCredentials.outputs;
final hasMultiDestination = outputs.length > 1; final hasMultiDestination = outputs.length > 1;
@ -284,7 +301,7 @@ abstract class ElectrumWalletBase
var fee = 0; var fee = 0;
if (hasMultiDestination) { if (hasMultiDestination) {
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 1)) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
} }
@ -324,15 +341,10 @@ abstract class ElectrumWalletBase
} }
} }
if (fee == 0 && networkType == bitcoin.bitcoin) { if (fee == 0) {
throw BitcoinTransactionWrongBalanceException(currency); throw BitcoinTransactionWrongBalanceException(currency);
} }
if (networkType == bitcoin.testnet) {
fee += 50;
amount -= 50;
}
final totalAmount = amount + fee; final totalAmount = amount + fee;
if ((totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) && if ((totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) &&
@ -656,6 +668,18 @@ abstract class ElectrumWalletBase
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> updateUnspent() async { Future<void> updateUnspent() async {
// Update unspents stored from scanned silent payment transactions
transactionHistory.transactions.values.forEach((tx) {
if (tx.unspent != null) {
if (!unspentCoins.any((utx) =>
utx.hash.contains(tx.unspent!.hash) &&
utx.vout == tx.unspent!.vout &&
utx.address.contains(tx.to!))) {
unspentCoins.add(tx.unspent!);
}
}
});
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient
.getListUnspentWithAddress(address.address, networkType) .getListUnspentWithAddress(address.address, networkType)
.then((unspent) => unspent.map((unspent) { .then((unspent) => unspent.map((unspent) {
@ -763,6 +787,7 @@ abstract class ElectrumWalletBase
} }
}); });
}); });
final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) {
try { try {
return fetchTransactionInfo( return fetchTransactionInfo(
@ -776,12 +801,25 @@ abstract class ElectrumWalletBase
return Future.value(null); return Future.value(null);
} }
})); }));
transactionHistory.transactions.values.forEach((tx) {
if (tx.direction == TransactionDirection.incoming &&
!walletAddresses.addresses.any((addr) => tx.to?.contains(addr.address) ?? false))
historiesWithDetails.add(tx);
});
return historiesWithDetails return historiesWithDetails
.fold<Map<String, ElectrumTransactionInfo>>(<String, ElectrumTransactionInfo>{}, (acc, tx) { .fold<Map<String, ElectrumTransactionInfo>>(<String, ElectrumTransactionInfo>{}, (acc, tx) {
if (tx == null) { if (tx == null) {
return acc; return acc;
} }
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
if (tx.to != null && tx.to!.isNotEmpty && acc[tx.id] != null) {
final updatedConf = acc[tx.id]!;
updatedConf.confirmations = walletInfo.restoreHeight - tx.height;
acc[tx.id] = updatedConf;
}
return acc; return acc;
}); });
} }
@ -824,6 +862,7 @@ abstract class ElectrumWalletBase
} }
}); });
}); });
await _chainTipUpdateSubject?.close(); await _chainTipUpdateSubject?.close();
_chainTipUpdateSubject = electrumClient.chainTipUpdate(); _chainTipUpdateSubject = electrumClient.chainTipUpdate();
_chainTipUpdateSubject?.listen((_) async { _chainTipUpdateSubject?.listen((_) async {
@ -856,15 +895,18 @@ abstract class ElectrumWalletBase
var totalConfirmed = 0; var totalConfirmed = 0;
var totalUnconfirmed = 0; var totalUnconfirmed = 0;
// Add values from unspent coins that are not fetched by the address list
// i.e. scanned silent payments
unspentCoinsInfo.values.forEach((info) { unspentCoinsInfo.values.forEach((info) {
unspentCoins.forEach((element) { unspentCoins.forEach((element) {
if (element.hash == info.hash && if (element.hash == info.hash &&
element.bitcoinAddressRecord.address == info.address && element.bitcoinAddressRecord.address == info.address &&
element.value == info.value) { element.value == info.value) {
if (info.isFrozen) totalFrozen += element.value; if (info.isFrozen) totalFrozen += element.value;
if (element.bitcoinAddressRecord.silentPaymentTweak != null) if (element.bitcoinAddressRecord.silentPaymentTweak != null) {
totalConfirmed += element.value; totalConfirmed += element.value;
} }
}
}); });
}); });
@ -982,8 +1024,7 @@ Future<ElectrumTransactionBundle> getTransactionExpanded(
class ScanData { class ScanData {
final SendPort sendPort; final SendPort sendPort;
final Uint8List scanPrivkeyCompressed; final bitcoin.SilentPaymentReceiver primarySilentAddress;
final Uint8List spendPubkeyCompressed;
final int height; final int height;
final String node; final String node;
final bitcoin.NetworkType networkType; final bitcoin.NetworkType networkType;
@ -994,8 +1035,7 @@ class ScanData {
ScanData({ ScanData({
required this.sendPort, required this.sendPort,
required this.scanPrivkeyCompressed, required this.primarySilentAddress,
required this.spendPubkeyCompressed,
required this.height, required this.height,
required this.node, required this.node,
required this.networkType, required this.networkType,
@ -1008,8 +1048,7 @@ class ScanData {
factory ScanData.fromHeight(ScanData scanData, int newHeight) { factory ScanData.fromHeight(ScanData scanData, int newHeight) {
return ScanData( return ScanData(
sendPort: scanData.sendPort, sendPort: scanData.sendPort,
scanPrivkeyCompressed: scanData.scanPrivkeyCompressed, primarySilentAddress: scanData.primarySilentAddress,
spendPubkeyCompressed: scanData.spendPubkeyCompressed,
height: newHeight, height: newHeight,
node: scanData.node, node: scanData.node,
networkType: scanData.networkType, networkType: scanData.networkType,
@ -1179,10 +1218,10 @@ Future<void> startRefresh(ScanData scanData) async {
// break; // break;
// } // }
// final p2tr = bitcoin.P2trAddress(program: script.sublist(2).hex); final p2tr = bitcoin.P2trAddress(program: script.sublist(2).hex);
// final address = p2tr.toAddress(scanData.networkType); final address = p2tr.toAddress(scanData.networkType);
// print(["Verifying taproot address:", address]); print(["Verifying taproot address:", address]);
outpointsByP2TRpubkey[script.sublist(2).hex] = outpointsByP2TRpubkey[script.sublist(2).hex] =
bitcoin.Outpoint(txid: txid, index: i, value: output["value"] as int); bitcoin.Outpoint(txid: txid, index: i, value: output["value"] as int);
@ -1198,8 +1237,10 @@ Future<void> startRefresh(ScanData scanData) async {
final curve = bitcoin.getSecp256k1(); final curve = bitcoin.getSecp256k1();
final result = bitcoin.scanOutputs( final result = bitcoin.scanOutputs(
bitcoin.PrivateKey.fromHex(curve, scanData.scanPrivkeyCompressed.hex), bitcoin.PrivateKey.fromHex(
bitcoin.PublicKey.fromHex(curve, scanData.spendPubkeyCompressed.hex), curve, scanData.primarySilentAddress.scanPrivkey.toCompressedHex()),
bitcoin.PublicKey.fromHex(
curve, scanData.primarySilentAddress.spendPubkey.toCompressedHex()),
bitcoin.getSumInputPubKeys(pubkeys), bitcoin.getSumInputPubKeys(pubkeys),
outpointHash, outpointHash,
outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(), outpointsByP2TRpubkey.keys.map((e) => e.fromHex).toList(),
@ -1225,24 +1266,54 @@ Future<void> startRefresh(ScanData scanData) async {
return; return;
} }
// found utxo for tx final tweak = value[0];
scanData.sendPort.send(BitcoinUnspent( String? label;
if (value.length > 1) label = value[1];
final unspent = BitcoinUnspent(
BitcoinAddressRecord( BitcoinAddressRecord(
bitcoin.P2trAddress(program: key, network: scanData.networkType) bitcoin.P2trAddress(program: key, network: scanData.networkType)
.toAddress(scanData.networkType), .toAddress(scanData.networkType),
index: 0, index: 0,
isHidden: false, isHidden: true,
isUsed: true, isUsed: true,
silentAddressLabel: null, silentAddressLabel: null,
silentPaymentTweak: value, silentPaymentTweak: tweak,
type: bitcoin.AddressType.p2tr, type: bitcoin.AddressType.p2tr,
), ),
outpoint.txid, txid,
outpoint.value!, outpoint.value!,
outpoint.index, outpoint.index,
silentPaymentTweak: value, silentPaymentTweak: tweak,
type: bitcoin.AddressType.p2tr, type: bitcoin.AddressType.p2tr,
)); );
// found utxo for tx, send unspent coin to main isolate
scanData.sendPort.send(unspent);
// also send tx data for tx history
scanData.sendPort.send({
txid: ElectrumTransactionInfo(
WalletType.bitcoin,
id: txid,
height: syncHeight,
amount: outpoint.value!,
fee: 0,
direction: TransactionDirection.incoming,
isPending: false,
date:
DateTime.fromMillisecondsSinceEpoch((blockJson["timestamp"] as int) * 1000),
confirmations: currentChainTip - syncHeight,
to: bitcoin.SilentPaymentAddress.createLabeledSilentPaymentAddress(
scanData.primarySilentAddress.scanPubkey,
scanData.primarySilentAddress.spendPubkey,
label != null ? label.fromHex : "0".fromHex,
hrp: scanData.primarySilentAddress.hrp,
version: scanData.primarySilentAddress.version)
.toString(),
unspent: unspent,
)
});
}); });
} catch (_) {} } catch (_) {}
} }

View file

@ -743,7 +743,7 @@
"unspent_change": "Mudar", "unspent_change": "Mudar",
"Block_remaining": "${status} bloco restante", "Block_remaining": "${status} bloco restante",
"labeled_silent_addresses": "Endereços silenciosos rotulados", "labeled_silent_addresses": "Endereços silenciosos rotulados",
"use_testnet": "Use testNet", "use_testnet": "Use Testnet",
"tor_connection": "Conexão Tor", "tor_connection": "Conexão Tor",
"seed_hex_form": "Semente de carteira (forma hexadecimal)", "seed_hex_form": "Semente de carteira (forma hexadecimal)",
"seedtype": "SeedType", "seedtype": "SeedType",