fix(wownero): hold the history mutex while reading transactions

The 1.0.5 build 15 macOS test crashed with SIGSEGV in
WOWNERO_TransactionInfo_amount on the main thread, near the end of a
restore sync. TransactionHistory_refresh runs on a background isolate
under txHistoryMutex and frees the TransactionInfo objects it replaces;
getAllSubaddresses() and getTransaction() read those objects without
the mutex, so a read that overlaps a refresh dereferences freed memory.
The wallet cache had not been stored, so the next launch synced again.

Upstream fixed the same race for Monero in #3188 and in the v6.4.5
coin-control change; this ports both to cw_wownero. getAllSubaddresses()
and getTransaction() take the mutex and become async, which carries
through the subaddress list, its app proxy and the configure interface.
The subaddress list now fetches before clearing, so the observable list
is never empty while the fetch is pending, and addSubaddress and
setLabelSubaddress await the update before callers read the new address.

The coin-control change check compared a Transaction with 1 and was
always false; it now uses the transaction's direction, as Monero does.
This commit is contained in:
jwinterm
2026-10-31 12:50:36 -04:00
parent d0f4e20b29
commit f08ae897e6
6 changed files with 51 additions and 35 deletions
+6 -1
View File
@@ -57,7 +57,9 @@ int lastWptr = 0;
int lastTxCount = 0; int lastTxCount = 0;
List<TinyTransactionDetails> ttDetails = []; List<TinyTransactionDetails> ttDetails = [];
List<Subaddress> getAllSubaddresses() { Future<List<Subaddress>> getAllSubaddresses() async {
await txHistoryMutex.acquire();
try {
txhistory = wownero.Wallet_history(wptr!); txhistory = wownero.Wallet_history(wptr!);
final txCount = wownero.TransactionHistory_count(txhistory!); final txCount = wownero.TransactionHistory_count(txhistory!);
if (lastTxCount != txCount && lastWptr != wptr!.address) { if (lastTxCount != txCount && lastWptr != wptr!.address) {
@@ -79,6 +81,9 @@ List<Subaddress> getAllSubaddresses() {
ttDetails.clear(); ttDetails.clear();
ttDetails.addAll(newttDetails); ttDetails.addAll(newttDetails);
} }
} finally {
txHistoryMutex.release();
}
final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex); final size = wownero.Wallet_numSubaddresses(wptr!, accountIndex: subaddress!.accountIndex);
final list = List.generate(size, (index) { final list = List.generate(size, (index) {
final ttDetailsLocal = ttDetails.where((element) { final ttDetailsLocal = ttDetails.where((element) {
+8 -2
View File
@@ -81,8 +81,14 @@ Future<List<Transaction>> getAllTransactions() async {
} }
// TODO(mrcyjanek): ... // TODO(mrcyjanek): ...
Transaction getTransaction(String txId) { Future<Transaction> getTransaction(String txId) async {
return Transaction(txInfo: wownero.TransactionHistory_transactionById(txhistory!, txid: txId)); await txHistoryMutex.acquire();
try {
return Transaction(
txInfo: wownero.TransactionHistory_transactionById(txhistory!, txid: txId));
} finally {
txHistoryMutex.release();
}
} }
Future<PendingTransactionDescription> createTransactionSync( Future<PendingTransactionDescription> createTransactionSync(
+9 -8
View File
@@ -24,7 +24,7 @@ abstract class WowneroSubaddressListBase with Store {
bool _isRefreshing; bool _isRefreshing;
bool _isUpdating; bool _isUpdating;
void update({required int accountIndex}) { Future<void> update({required int accountIndex}) async {
refreshCoins(accountIndex); refreshCoins(accountIndex);
if (_isUpdating) { if (_isUpdating) {
@@ -34,8 +34,9 @@ abstract class WowneroSubaddressListBase with Store {
try { try {
_isUpdating = true; _isUpdating = true;
refresh(accountIndex: accountIndex); refresh(accountIndex: accountIndex);
final newAddrs = await getAll();
subaddresses.clear(); subaddresses.clear();
subaddresses.addAll(getAll()); subaddresses.addAll(newAddrs);
_isUpdating = false; _isUpdating = false;
} catch (e) { } catch (e) {
_isUpdating = false; _isUpdating = false;
@@ -43,8 +44,8 @@ abstract class WowneroSubaddressListBase with Store {
} }
} }
List<Subaddress> getAll() { Future<List<Subaddress>> getAll() async {
var subaddresses = subaddress_list.getAllSubaddresses(); var subaddresses = await subaddress_list.getAllSubaddresses();
if (subaddresses.length > 2) { if (subaddresses.length > 2) {
final primary = subaddresses.first; final primary = subaddresses.first;
@@ -74,14 +75,14 @@ abstract class WowneroSubaddressListBase with Store {
Future<void> addSubaddress({required int accountIndex, required String label}) async { Future<void> addSubaddress({required int accountIndex, required String label}) async {
await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
update(accountIndex: accountIndex); await update(accountIndex: accountIndex);
} }
Future<void> setLabelSubaddress( Future<void> setLabelSubaddress(
{required int accountIndex, required int addressIndex, required String label}) async { {required int accountIndex, required int addressIndex, required String label}) async {
await subaddress_list.setLabelForSubaddress( await subaddress_list.setLabelForSubaddress(
accountIndex: accountIndex, addressIndex: addressIndex, label: label); accountIndex: accountIndex, addressIndex: addressIndex, label: label);
update(accountIndex: accountIndex); await update(accountIndex: accountIndex);
} }
void refresh({required int accountIndex}) { void refresh({required int accountIndex}) {
@@ -129,7 +130,7 @@ abstract class WowneroSubaddressListBase with Store {
Future<List<Subaddress>> _getAllUnusedAddresses( Future<List<Subaddress>> _getAllUnusedAddresses(
{required int accountIndex, required String label}) async { {required int accountIndex, required String label}) async {
final allAddresses = subaddress_list.getAllSubaddresses(); final allAddresses = await subaddress_list.getAllSubaddresses();
final lastAddress = allAddresses.last.address; final lastAddress = allAddresses.last.address;
if (allAddresses.isEmpty || _usedAddresses.contains(lastAddress)) { if (allAddresses.isEmpty || _usedAddresses.contains(lastAddress)) {
final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label);
@@ -156,7 +157,7 @@ abstract class WowneroSubaddressListBase with Store {
Future<bool> _newSubaddress({required int accountIndex, required String label}) async { Future<bool> _newSubaddress({required int accountIndex, required String label}) async {
await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label);
return subaddress_list.getAllSubaddresses().where((s) { return (await subaddress_list.getAllSubaddresses()).where((s) {
final address = s.address; final address = s.address;
return !_usedAddresses.contains(address); return !_usedAddresses.contains(address);
}).isNotEmpty; }).isNotEmpty;
+6 -1
View File
@@ -583,7 +583,12 @@ abstract class WowneroWalletBase
wownero.CoinsInfo_unlocked(coin), wownero.CoinsInfo_unlocked(coin),
); );
if (unspent.hash.isNotEmpty) { if (unspent.hash.isNotEmpty) {
unspent.isChange = transaction_history.getTransaction(unspent.hash) == 1; try {
final tx = await transaction_history.getTransaction(unspent.hash);
unspent.isChange = tx.isSpend == true;
} catch (e) {
printV("isChange lookup failed for ${unspent.hash}: $e");
}
} }
unspentCoins.add(unspent); unspentCoins.add(unspent);
} }
+2 -3
View File
@@ -83,10 +83,9 @@ class CWWowneroSubaddressList extends WowneroSubaddressList {
} }
@override @override
List<Subaddress> getAll(Object wallet) { Future<List<Subaddress>> getAll(Object wallet) async {
final wowneroWallet = wallet as WowneroWallet; final wowneroWallet = wallet as WowneroWallet;
return wowneroWallet.walletAddresses.subaddressList return (await wowneroWallet.walletAddresses.subaddressList.getAll())
.getAll()
.map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address)) .map((sub) => Subaddress(id: sub.id, label: sub.label, address: sub.address))
.toList(); .toList();
} }
+1 -1
View File
@@ -658,7 +658,7 @@ abstract class WowneroSubaddressList {
ObservableList<Subaddress> get subaddresses; ObservableList<Subaddress> get subaddresses;
void update(Object wallet, {required int accountIndex}); void update(Object wallet, {required int accountIndex});
void refresh(Object wallet, {required int accountIndex}); void refresh(Object wallet, {required int accountIndex});
List<Subaddress> getAll(Object wallet); Future<List<Subaddress>> getAll(Object wallet);
Future<void> addSubaddress(Object wallet, {required int accountIndex, required String label}); Future<void> addSubaddress(Object wallet, {required int accountIndex, required String label});
Future<void> setLabelSubaddress(Object wallet, Future<void> setLabelSubaddress(Object wallet,
{required int accountIndex, required int addressIndex, required String label}); {required int accountIndex, required int addressIndex, required String label});