Skip to content

Commit 835b5ab

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 835b5ab

1 file changed

Lines changed: 81 additions & 72 deletions

File tree

lib/wallets/wallet/wallet.dart

Lines changed: 81 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -641,95 +641,104 @@ 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 Future<void>(() async {
646+
// add some small buffer before making calls.
647+
// this can probably be removed in the future but was added as a
648+
// debugging feature
649+
await Future<void>.delayed(const Duration(milliseconds: 300));
650+
651+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
652+
final Set<String> codesToCheck = {};
653+
if (this is PaynymInterface && !viewOnly) {
654+
// isSegwit does not matter here at all
655+
final myCode = await (this as PaynymInterface).getPaymentCode(
656+
isSegwit: false,
657+
);
656658

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);
659+
final nym = await PaynymIsApi().nym(myCode.toString());
660+
if (nym.value != null) {
661+
for (final follower in nym.value!.followers) {
662+
codesToCheck.add(follower.code);
663+
}
664+
for (final following in nym.value!.following) {
665+
codesToCheck.add(following.code);
666+
}
664667
}
665668
}
666-
}
667669

668-
_fireRefreshPercentChange(0);
669-
await updateChainHeight();
670+
_fireRefreshPercentChange(0);
671+
await updateChainHeight();
670672

671-
if (this is BitcoinFrostWallet) {
672-
await (this as BitcoinFrostWallet).lookAhead();
673-
}
673+
if (this is BitcoinFrostWallet) {
674+
await (this as BitcoinFrostWallet).lookAhead();
675+
}
674676

675-
_fireRefreshPercentChange(0.1);
677+
_fireRefreshPercentChange(0.1);
676678

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();
679+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
680+
if (this is MultiAddressInterface) {
681+
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
682+
await (this as MultiAddressInterface)
683+
.checkReceivingAddressForTransactions();
684+
}
682685
}
683-
}
684686

685-
_fireRefreshPercentChange(0.2);
687+
_fireRefreshPercentChange(0.2);
686688

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();
689+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
690+
if (this is MultiAddressInterface) {
691+
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
692+
await (this as MultiAddressInterface)
693+
.checkChangeAddressForTransactions();
694+
}
695+
}
696+
_fireRefreshPercentChange(0.3);
697+
if (this is SparkInterface && !viewOnly) {
698+
// this should be called before updateTransactions()
699+
await (this as SparkInterface).refreshSparkData((0.3, 0.6));
692700
}
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-
}
699701

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;
714-
}
702+
if (this is NamecoinWallet) {
703+
await updateUTXOs();
704+
_fireRefreshPercentChange(0.6);
705+
await (this as NamecoinWallet).checkAutoRegisterNameNewOutputs();
706+
_fireRefreshPercentChange(0.70);
707+
await updateTransactions();
708+
} else {
709+
final fetchFuture = updateTransactions();
710+
_fireRefreshPercentChange(0.6);
711+
final utxosRefreshFuture = updateUTXOs();
712+
await utxosRefreshFuture;
713+
_fireRefreshPercentChange(0.65);
714+
await fetchFuture;
715+
_fireRefreshPercentChange(0.70);
716+
}
715717

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();
723-
}
724-
_fireRefreshPercentChange(0.80);
718+
// TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
719+
if (!viewOnly && this is PaynymInterface && codesToCheck.isNotEmpty) {
720+
await (this as PaynymInterface).checkForNotificationTransactionsTo(
721+
codesToCheck,
722+
);
723+
// check utxos again for notification outputs
724+
await updateUTXOs();
725+
}
726+
_fireRefreshPercentChange(0.80);
725727

726-
// await getAllTxsToWatch();
728+
// await getAllTxsToWatch();
727729

728-
_fireRefreshPercentChange(0.90);
730+
_fireRefreshPercentChange(0.90);
729731

730-
await updateBalance();
732+
await updateBalance();
731733

732-
_fireRefreshPercentChange(1.0);
734+
_fireRefreshPercentChange(1.0);
735+
}).timeout(
736+
refreshTimeout,
737+
onTimeout: () => throw TimeoutException(
738+
'Wallet refresh timed out for $walletId',
739+
refreshTimeout,
740+
),
741+
);
733742

734743
completer.complete();
735744
} catch (error, strace) {

0 commit comments

Comments
 (0)