Skip to content

fix: match cjit entry to channel by funding tx#1017

Open
jvsena42 wants to merge 16 commits into
masterfrom
fix/cjit-channel-false-match
Open

fix: match cjit entry to channel by funding tx#1017
jvsena42 wants to merge 16 commits into
masterfrom
fix/cjit-channel-false-match

Conversation

@jvsena42

@jvsena42 jvsena42 commented Jun 16, 2026

Copy link
Copy Markdown
Member

This PR fixes the "Spending Balance Ready" confirmation not appearing after a successful Transfer to Spending when an unused instant-payment (CJIT) invoice is still pending.

Description

When a spending channel opens, the app decides whether the new channel came from a CJIT (instant-payment) invoice or from a paid channel order. Previously this match was done by comparing only the channel size and the LSP node pubkey. Because both flows use the same LSP and the same default channel sizing, a leftover unpaid CJIT entry of the same size would be mistaken for the freshly opened channel-order channel.

The side effects of that false match were:

  • the "Spending Balance Ready" toast was suppressed, and
  • the transfer was incorrectly treated as an incoming payment (a received transaction sheet and a spurious received Lightning activity).

The CJIT entry is now matched to a channel by its funding transaction id, which is the stable identifier that actually ties a CJIT entry to the channel it opened (mirroring the iOS matching). Entries are refreshed from the server first so a genuinely opened CJIT channel association is current before matching, with a fallback to cached state. A stale, unpaid CJIT entry has no opened channel and can no longer be mistaken for a channel-order channel.

Preview

without-cjit.webm
bug.webm
fix.webm
cjit.webm

QA Notes

Manual Tests

  • 1. Create an instant-payment invoice (Receive → Spending → enter amount → generate) but do not pay it, then Transfer to Spending the same amount → Confirm → swipe: "Spending Balance Ready" confirmation appears (not a received-payment sheet), and no spurious received activity is added.
  • 2. regression: No leftover CJIT invoice → Transfer to Spending → Confirm → swipe: "Spending Balance Ready" confirmation still appears.
  • 3. regression: Pay a real instant-payment (CJIT) invoice → channel opens: received-payment sheet and received Lightning activity still show as before.

Automated Checks

  • Unit tests added: cover CJIT-to-channel matching by funding tx in app/src/test/java/to/bitkit/repositories/BlocktankRepoTest.kt — stale unpaid entry is not matched, funding-tx match/no-match, and null funding txo.
  • CI: standard compile, unit test, and detekt checks run by the PR bot.

@jvsena42 jvsena42 self-assigned this Jun 16, 2026
@jvsena42 jvsena42 marked this pull request as ready for review June 18, 2026 12:20
@jvsena42 jvsena42 enabled auto-merge June 18, 2026 12:21
@jvsena42 jvsena42 added this to the 2.4.0 milestone Jun 18, 2026
@greptile-apps

greptile-apps Bot commented Jun 18, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes a false-positive CJIT match that suppressed the "Spending Balance Ready" toast and generated a spurious received-payment entry whenever an unpaid instant-payment invoice was still pending during a Transfer to Spending. The matching logic in getCjitEntry is rewritten to compare the full funding outpoint (txid + vout) instead of channel size and LSP pubkey, mirroring the iOS implementation.

  • BlocktankRepo.getCjitEntry now returns early on a cached hit, skips the server round-trip when no entry is genuinely awaiting a channel, and uses a new refreshCjitEntries helper with 3 retries and a 5 s per-attempt timeout to fetch fresh state before matching.
  • runSuspendCatching is introduced in ext/Coroutines.kt and adopted in NotifyChannelReadyHandler so CancellationException is no longer silently swallowed; the existing withTimeout-as-retriable-failure pattern in refreshCjitEntries is explicitly documented in AGENTS.md as the sanctioned exception.
  • Nine new unit tests cover null funding txo, stale/expired entry non-matching, txid+vout exact matching, cached hit without refresh, total refresh failure, and transient-error retry.

Confidence Score: 5/5

Safe to merge — the fix is well-scoped, mirrors the iOS behaviour, and is guarded by comprehensive unit tests covering the key edge cases.

The changed paths are all covered by new unit tests; the matching logic itself is simple and provably correct (exact outpoint comparison); the runSuspendCatching adoption removes a latent cancellation-swallowing hazard; and the structured-concurrency exception in refreshCjitEntries is explicitly documented. The one nit (unnecessary post-last-attempt delay) has no correctness impact.

app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt — specifically the retry loop in refreshCjitEntries where the trailing delay fires even after the final attempt.

Important Files Changed

Filename Overview
app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Core fix: getCjitEntry now matches by funding outpoint (txid+vout) instead of channel size/LSP pubkey. Adds a refreshCjitEntries helper with retry logic and the documented runCatching+explicit-rethrow pattern for timeout-as-retriable-failure; minor wasted delay after the last retry attempt.
app/src/main/java/to/bitkit/domain/commands/NotifyChannelReadyHandler.kt Migrated from runCatching to runSuspendCatching so coroutine cancellation is no longer swallowed; all return@ labels updated accordingly.
app/src/main/java/to/bitkit/ext/Coroutines.kt Adds runSuspendCatching, an inline wrapper that re-throws CancellationException before wrapping any other Throwable in Result.failure.
app/src/test/java/to/bitkit/repositories/BlocktankRepoTest.kt Comprehensive new test coverage: null funding txo, stale unpaid entry, txid+vout match/no-match, vout-only mismatch, no-pending-CJIT skip, expired entry skip, cached hit without refresh, total refresh failure, and retry-on-transient-error.
app/src/test/java/to/bitkit/ext/CoroutinesTest.kt Unit tests for runSuspendCatching: success wrapping, non-cancellation failure wrapping, and CancellationException re-throw — all three contract points covered.
AGENTS.md Documents the new runSuspendCatching rule and the withTimeout exception pattern; canonicalises BlocktankRepo.refreshCjitEntries as the documented exception site.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["getCjitEntry(channel)"] --> B{"channel.fundingTxo == null?"}
    B -- Yes --> C["return null"]
    B -- No --> D["check cached cjitEntries for txid+vout match"]
    D -- Match found --> E["return cached entry"]
    D -- No match --> F{"any cached entry has channel==null AND state != EXPIRED/FAILED?"}
    F -- No --> G["return null (skip server round-trip)"]
    F -- Yes --> H["refreshCjitEntries()"]
    H --> I["withTimeout(5s): cjitEntries(refresh=true)"]
    I -- Success --> J["update _blocktankState, return fresh list"]
    I -- Timeout or network error --> K{"attempt < 3?"}
    K -- Yes --> L["delay(1s) → retry"]
    L --> I
    K -- No --> M["Logger.warn, return cached list"]
    J --> N{"fresh list contains txid+vout match?"}
    M --> N
    N -- Yes --> O["return matching entry"]
    N -- No --> P["return null"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["getCjitEntry(channel)"] --> B{"channel.fundingTxo == null?"}
    B -- Yes --> C["return null"]
    B -- No --> D["check cached cjitEntries for txid+vout match"]
    D -- Match found --> E["return cached entry"]
    D -- No match --> F{"any cached entry has channel==null AND state != EXPIRED/FAILED?"}
    F -- No --> G["return null (skip server round-trip)"]
    F -- Yes --> H["refreshCjitEntries()"]
    H --> I["withTimeout(5s): cjitEntries(refresh=true)"]
    I -- Success --> J["update _blocktankState, return fresh list"]
    I -- Timeout or network error --> K{"attempt < 3?"}
    K -- Yes --> L["delay(1s) → retry"]
    L --> I
    K -- No --> M["Logger.warn, return cached list"]
    J --> N{"fresh list contains txid+vout match?"}
    M --> N
    N -- Yes --> O["return matching entry"]
    N -- No --> P["return null"]
Loading

Reviews (4): Last reviewed commit: "Merge branch 'master' into fix/cjit-chan..." | Re-trigger Greptile

Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b143259f3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
@jvsena42 jvsena42 marked this pull request as draft June 18, 2026 12:39
auto-merge was automatically disabled June 18, 2026 12:39

Pull request was converted to draft

@jvsena42

This comment was marked as resolved.

@jvsena42 jvsena42 marked this pull request as ready for review June 18, 2026 13:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae7baa7b7d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
@jvsena42 jvsena42 marked this pull request as draft June 18, 2026 13:24
@jvsena42 jvsena42 marked this pull request as ready for review June 18, 2026 13:42

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a884da4459

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
@jvsena42

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85d27e6d75

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/BlocktankRepo.kt Outdated
jvsena42 added 2 commits June 18, 2026 12:02
… to the same wallet into one funding transaction while two CJIT invoices are pending, so their entries share a txid and differ only by vout
@jvsena42 jvsena42 requested a review from ovitrif June 18, 2026 16:39
ovitrif

This comment was marked as resolved.

@jvsena42 jvsena42 marked this pull request as draft June 19, 2026 11:54
@jvsena42

Copy link
Copy Markdown
Member Author

on draft to apply suggestion

@jvsena42 jvsena42 marked this pull request as ready for review June 19, 2026 13:13
@jvsena42

Copy link
Copy Markdown
Member Author

Videos updated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fceabe494

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


fun List<IcJitEntry>.matching(): IcJitEntry? = firstOrNull { entry ->
val fundingTx = entry.channel?.fundingTx ?: return@firstOrNull false
fundingTx.id == fundingTxo.txid && fundingTx.vout == fundingTxo.vout.toULong()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use vout in CJIT activity dedupe too

When Blocktank batches two CJIT channels in one funding transaction, this matcher now correctly distinguishes them by vout, but the matched entry is immediately recorded through ActivityRepo.insertActivityFromCjit, which still uses only channel.fundingTxo?.txid as the activity/dedup id (ActivityRepo.kt:627). Both outputs therefore share the same activity id, so after the first channel-ready event inserts an activity, the second is treated as Duplicate and its receive sheet/notification is suppressed; please carry the output index into the downstream activity id/dedup key as well.

Useful? React with 👍 / 👎.

val hasPendingCjit = cached.any {
it.channel == null && it.state != CJitStateEnum.EXPIRED && it.state != CJitStateEnum.FAILED
}
if (!hasPendingCjit) return@withContext null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh when the CJIT cache is empty

When a paid CJIT is handled before _blocktankState.cjitEntries has been populated (for example immediately after createCjit(), which only starts refreshOrders() asynchronously, or after a cold start/wake), cached is empty so hasPendingCjit is false and this returns without the server refresh that would contain the matching funding tx. That makes the channel-ready path treat a real CJIT as a normal channel open and skip the received-payment activity/notification; avoid making an uninitialized/empty cache a terminal no-match.

Useful? React with 👍 / 👎.

Sign up for free to 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.

2 participants