') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat: one-time stale channel monitor recovery (v2) by jvsena42 · Pull Request #501 · synonymdev/bitkit-ios · GitHub
Skip to content

feat: one-time stale channel monitor recovery (v2) - #501

Closed
jvsena42 wants to merge 1 commit into
masterfrom
fix/stale-monitor-recovery-v2
Closed

feat: one-time stale channel monitor recovery (v2)#501
jvsena42 wants to merge 1 commit into
masterfrom
fix/stale-monitor-recovery-v2

Conversation

@jvsena42

Copy link
Copy Markdown
Member

Description

This PR adds automatic one-time recovery for users affected by stale channel monitors caused by the RN migration overwrite bug (#462, #495).

On BuildError.ReadFailed, the app automatically retries the LDK node build once with accept_stale_channel_monitors enabled. A persisted staleMonitorRecoveryAttempted UserDefaults flag ensures this only happens once — the flag is set on any successful build, so unaffected users see zero impact.

This is a clean cherry-pick of the recovery logic from #500 onto release-2.1.1, without the unrelated pubky work that was in the original branch.

Linked Issues/Tasks

Screenshot / Video

N/A - backend logic change

QA Notes

  1. Reproduce the stale monitor state (overwrite a channel monitor in VSS with an older update_id)
  2. Launch the app — first build fails with ReadFailed
  3. Verify the retry succeeds and the node starts
  4. Check logs for "Stale monitor recovery: build succeeded with accept_stale"
  5. Kill and relaunch — verify normal startup (no retry triggered)

🤖 Generated with Claude Code

On BuildError.ReadFailed (likely stale ChannelMonitor from migration
overwrite), automatically retry once with accept_stale_channel_monitors
enabled. The ldk-node recovery flag force-syncs the monitor's update_id
and heals commitment state via a delayed chain sync + keysend round-trip.
A persisted UserDefaults flag ensures this only triggers once — set on
any successful build (affected or not), preventing future retries.
Depends on: synonymdev/ldk-node#76
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 marked this pull request as draft March 18, 2026 16:39
@jvsena42

Copy link
Copy Markdown
MemberAuthor

on draft to wait for bindings

throw error
}

// Build failed with ReadFailed — likely a stale ChannelMonitor (DangerousValue).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with specific exception

context: "Recovery"
)
Self.staleMonitorRecoveryAttempted = true
builder.setAcceptStaleChannelMonitors(accept: true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing dependency: setAcceptStaleChannelMonitors not in pinned ldk-node revision

This call references builder.setAcceptStaleChannelMonitors(accept: true), but the method does not exist in the currently pinned synonymdev/ldk-node revision (c5698d0, set in Bitkit.xcodeproj). As a result, this branch will not compile until the project.pbxproj is updated to a commit that includes synonymdev/ldk-node#76 (which the PR description already calls out as a requirement).

)
Self.staleMonitorRecoveryAttempted =true
builder.setAcceptStaleChannelMonitors(accept:true)
if !lnurlAuthServerUrl.isEmpty {

@ovitrif

Copy link
Copy Markdown
Collaborator

best to align this with the android counterpart:

Base automatically changed from release-2.1.1 to masterMarch 19, 2026 08:18
@jvsena42

Copy link
Copy Markdown
MemberAuthor

replaced by #502

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@jvsena42@ovitrif@ben-kaufman