Skip to content

Commit 712763c

Browse files
reubenyapclaude
andcommitted
fix: prevent Firo sync stall at ~65% with refresh timeout and progress reorder
Two targeted changes to _refresh() in wallet.dart: 1. Master 5-minute timeout on the refresh body. If any sub-operation hangs (unresponsive server, OS-suspended socket, etc.), TimeoutException now unwinds through the existing catch/finally and releases refreshMutex. Previously the mutex would remain locked forever, making every subsequent periodic sync bail out at the isLocked check until the app was force-closed. 2. Fire progress updates *after* the awaited work completes rather than before. The 0.65 and 0.70 calls previously fired immediately after kicking off updateUTXOs/updateTransactions, making the bar appear stuck at 65% while the actual work was still running. Note: .timeout() does not cancel in-flight work; it only completes the outer future. The intent is to recover the mutex — connection-level stall detection is still handled by the electrum adapter's existing connectionTimeout and aliveTimerDuration (60s each). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 243015b commit 712763c

1 file changed

Lines changed: 99 additions & 88 deletions

File tree

lib/wallets/wallet/wallet.dart

Lines changed: 99 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -641,113 +641,124 @@ abstract class Wallet<T extends CryptoCurrency> {
641641
);
642642
}
643643

644-
// add some small buffer before making calls.
645-
// this can probably be removed in the future but was added as a
646-
// debugging feature
647-
await Future<void>.delayed(const Duration(milliseconds: 300));
648-
649-
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
650-
final Set<String> codesToCheck = {};
651-
if (this is PaynymInterface && !viewOnly) {
652-
// isSegwit does not matter here at all
653-
final myCode = await (this as PaynymInterface).getPaymentCode(
654-
isSegwit: false,
655-
);
644+
const refreshTimeout = Duration(minutes: 5);
645+
await _doRefreshWork(viewOnly).timeout(
646+
refreshTimeout,
647+
onTimeout: () => throw TimeoutException(
648+
'Wallet refresh timed out for $walletId',
649+
refreshTimeout,
650+
),
651+
);
656652

657-
final nym = await PaynymIsApi().nym(myCode.toString());
658-
if (nym.value != null) {
659-
for (final follower in nym.value!.followers) {
660-
codesToCheck.add(follower.code);
661-
}
662-
for (final following in nym.value!.following) {
663-
codesToCheck.add(following.code);
664-
}
665-
}
653+
completer.complete();
654+
} catch (error, strace) {
655+
completer.completeError(error, strace);
656+
} finally {
657+
refreshMutex.release();
658+
if (!completer.isCompleted) {
659+
completer.completeError(
660+
"finally block hit before completer completed",
661+
StackTrace.current,
662+
);
666663
}
667664

668-
_fireRefreshPercentChange(0);
669-
await updateChainHeight();
670-
671-
if (this is BitcoinFrostWallet) {
672-
await (this as BitcoinFrostWallet).lookAhead();
673-
}
665+
Logging.instance.i(
666+
"Refresh for "
667+
"$walletId::${info.name}: ${DateTime.now().difference(start)}",
668+
);
669+
}
670+
}
674671

675-
_fireRefreshPercentChange(0.1);
672+
Future<void> _doRefreshWork(bool viewOnly) async {
673+
// add some small buffer before making calls.
674+
// this can probably be removed in the future but was added as a
675+
// debugging feature
676+
await Future<void>.delayed(const Duration(milliseconds: 300));
677+
678+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
679+
final Set<String> codesToCheck = {};
680+
if (this is PaynymInterface && !viewOnly) {
681+
// isSegwit does not matter here at all
682+
final myCode = await (this as PaynymInterface).getPaymentCode(
683+
isSegwit: false,
684+
);
676685

677-
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
678-
if (this is MultiAddressInterface) {
679-
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
680-
await (this as MultiAddressInterface)
681-
.checkReceivingAddressForTransactions();
686+
final nym = await PaynymIsApi().nym(myCode.toString());
687+
if (nym.value != null) {
688+
for (final follower in nym.value!.followers) {
689+
codesToCheck.add(follower.code);
690+
}
691+
for (final following in nym.value!.following) {
692+
codesToCheck.add(following.code);
682693
}
683694
}
695+
}
684696

685-
_fireRefreshPercentChange(0.2);
697+
_fireRefreshPercentChange(0);
698+
await updateChainHeight();
686699

687-
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
688-
if (this is MultiAddressInterface) {
689-
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
690-
await (this as MultiAddressInterface)
691-
.checkChangeAddressForTransactions();
692-
}
693-
}
694-
_fireRefreshPercentChange(0.3);
695-
if (this is SparkInterface && !viewOnly) {
696-
// this should be called before updateTransactions()
697-
await (this as SparkInterface).refreshSparkData((0.3, 0.6));
698-
}
700+
if (this is BitcoinFrostWallet) {
701+
await (this as BitcoinFrostWallet).lookAhead();
702+
}
699703

700-
if (this is NamecoinWallet) {
701-
await updateUTXOs();
702-
_fireRefreshPercentChange(0.6);
703-
await (this as NamecoinWallet).checkAutoRegisterNameNewOutputs();
704-
_fireRefreshPercentChange(0.70);
705-
await updateTransactions();
706-
} else {
707-
final fetchFuture = updateTransactions();
708-
_fireRefreshPercentChange(0.6);
709-
final utxosRefreshFuture = updateUTXOs();
710-
_fireRefreshPercentChange(0.65);
711-
await utxosRefreshFuture;
712-
_fireRefreshPercentChange(0.70);
713-
await fetchFuture;
704+
_fireRefreshPercentChange(0.1);
705+
706+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
707+
if (this is MultiAddressInterface) {
708+
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
709+
await (this as MultiAddressInterface)
710+
.checkReceivingAddressForTransactions();
714711
}
712+
}
715713

716-
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
717-
if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) {
718-
await (this as PaynymInterface).checkForNotificationTransactionsTo(
719-
codesToCheck,
720-
);
721-
// check utxos again for notification outputs
722-
await updateUTXOs();
714+
_fireRefreshPercentChange(0.2);
715+
716+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
717+
if (this is MultiAddressInterface) {
718+
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
719+
await (this as MultiAddressInterface)
720+
.checkChangeAddressForTransactions();
723721
}
724-
_fireRefreshPercentChange(0.80);
722+
}
723+
_fireRefreshPercentChange(0.3);
724+
if (this is SparkInterface && !viewOnly) {
725+
// this should be called before updateTransactions()
726+
await (this as SparkInterface).refreshSparkData((0.3, 0.6));
727+
}
725728

726-
// await getAllTxsToWatch();
729+
if (this is NamecoinWallet) {
730+
await updateUTXOs();
731+
_fireRefreshPercentChange(0.6);
732+
await (this as NamecoinWallet).checkAutoRegisterNameNewOutputs();
733+
_fireRefreshPercentChange(0.70);
734+
await updateTransactions();
735+
} else {
736+
final fetchFuture = updateTransactions();
737+
_fireRefreshPercentChange(0.6);
738+
final utxosRefreshFuture = updateUTXOs();
739+
await utxosRefreshFuture;
740+
_fireRefreshPercentChange(0.65);
741+
await fetchFuture;
742+
_fireRefreshPercentChange(0.70);
743+
}
727744

728-
_fireRefreshPercentChange(0.90);
745+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
746+
if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) {
747+
await (this as PaynymInterface).checkForNotificationTransactionsTo(
748+
codesToCheck,
749+
);
750+
// check utxos again for notification outputs
751+
await updateUTXOs();
752+
}
753+
_fireRefreshPercentChange(0.80);
729754

730-
await updateBalance();
755+
// await getAllTxsToWatch();
731756

732-
_fireRefreshPercentChange(1.0);
757+
_fireRefreshPercentChange(0.90);
733758

734-
completer.complete();
735-
} catch (error, strace) {
736-
completer.completeError(error, strace);
737-
} finally {
738-
refreshMutex.release();
739-
if (!completer.isCompleted) {
740-
completer.completeError(
741-
"finally block hit before completer completed",
742-
StackTrace.current,
743-
);
744-
}
759+
await updateBalance();
745760

746-
Logging.instance.i(
747-
"Refresh for "
748-
"$walletId::${info.name}: ${DateTime.now().difference(start)}",
749-
);
750-
}
761+
_fireRefreshPercentChange(1.0);
751762
}
752763

753764
Future<void> exit() async {

0 commit comments

Comments
 (0)