Skip to content

fix(speed-up): resolve BTC RBF address paths and refine action UX#12381

Merged
kaladinlight merged 4 commits into
developfrom
fix/btc-speed-up-address-resolution
May 26, 2026
Merged

fix(speed-up): resolve BTC RBF address paths and refine action UX#12381
kaladinlight merged 4 commits into
developfrom
fix/btc-speed-up-address-resolution

Conversation

@kaladinlight
Copy link
Copy Markdown
Member

@kaladinlight kaladinlight commented May 26, 2026

Description

Fixes BTC RBF (speed-up) signing failures and tightens the RBF action UX.

Path resolution

The speed-up modal was deriving each input's BIP44 path by treating an address's position in unchained's combined receive+change list as its BIP44 addressIndex. The two have no defined relation, so signing failed with Can not sign for this input with the key X whenever the metadata captured at original-send time wasn't available.

Each vin's addressNList is now resolved via account.chainSpecific.addresses[].path (adds chain-adapters Address.path; requires the matching unchained path passthrough), current UTXO paths, and stored metadata, with a clear error thrown if none match — no more brute-force candidate guessing.

Output classification

With unchained now returning a BIP44 path on every xpub-derived address, each vout is classified deterministically via addressNList[3] === 1, dropping the intendedSendSats amount-match heuristic and its known self-send / same-value edge cases. Input addressNLists are resolved from the account's owned-address map alone, removing the redundant getUtxos round trip.

Modal refactor

  • useQuery replaces the manual fetch effect + refs + withRetry
  • Pure helpers (summarizeOriginalTx, classifyOriginalOutputs, reconstructReplacementInputs) live in speedUpUtils.ts
  • Bitcoin base asset constant replaces the BTC_ASSET_ID/store-fetched btcAsset pair
  • Misleading assetId prop dropped since the modal is BTC-only

RBF action UX

  • New transactionHistory.replaced translation so the replaced action reads "You sped up your send of X BTC" instead of the bare "Replaced", which doubled up with the status pill
  • Replaced status pill switches from yellow to gray so it's visually distinct from Pending
  • Dispatches the Replaced upsert before the new Pending insert so the reducer's auto-stamp orders the new tx above the replaced one

Adds unit coverage for classifyOriginalOutputs and reconstructReplacementInputs (chain-index branches, metadata priority, multi-vin indexing, empty-array guard, throw paths).

Issue (if applicable)

closes #

Risk

Medium. Touches on-chain transaction construction (RBF replacement signing) for BTC. The fix is narrowly scoped to address-path resolution and output classification, but anyone speeding up a BTC tx exercises this code.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

  • Bitcoin RBF (speed-up) flow only
  • All wallets that support BTC sends (Native, Ledger, KeepKey)

Testing

Engineering

  1. Send a BTC tx at a low fee rate so it sits in the mempool
  2. Open the action center, find the pending action, click Speed Up
  3. Verify:
    • The original tx summary (fee, vsize, fee rate) is correct
    • Change outputs are correctly identified (only addressNList[3] === 1)
    • The replacement tx signs and broadcasts successfully
    • The Replaced action shows "You sped up your send of X BTC" with a gray status pill
    • The new Pending action sits above the Replaced one in the list
  4. Repeat with:
    • A wallet that does NOT have the original tx's metadata cached (clear localStorage / fresh load) — should still resolve via account address paths
    • A self-send (same address receive+change) — should no longer misclassify
  5. Run unit tests: pnpm jest src/components/Layout/Header/ActionCenter/components/speedUpUtils.test.ts

Operations

  • 🏁 My feature is behind a flag and doesn't require operations testing (yet)

Not flagged. QA should regression-test the BTC RBF flow end-to-end on a preview environment with each supported wallet (Native, Ledger, KeepKey).

Screenshots (if applicable)

Summary by CodeRabbit

  • New Features

    • Improved Speed Up flow with more reliable transaction reconstruction and fee selection.
  • Bug Fixes

    • Prevents invalid replacement attempts and ensures proper pending→replaced/ pending replacement handling.
    • Better handling of change/output edge cases and dust rules.
  • Documentation

    • Added English messages for Speed Up errors and a clearer "You sped up your send of %{amount} %{symbol}" replacement message.
  • Style

    • Changed replaced-action visual tag color from yellow to gray.
  • Tests

    • Expanded unit coverage for Speed Up output classification and input reconstruction.

Review Change Stack

kaladinlight and others added 2 commits May 21, 2026 17:25
…osition

The BTC speed-up modal was deriving each input's BIP44 path by treating
an address's position in unchained's combined receive+change list as its
BIP44 addressIndex. The two have no defined relation, so signing failed
with "Can not sign for this input with the key X" whenever the metadata
captured at original-send time wasn't available.

Resolve each vin's addressNList from `account.chainSpecific.addresses[].path`
(adds chain-adapters `Address.path`; requires the matching unchained `path`
passthrough), current UTXO paths, and stored metadata. Throw a clear error
if none match instead of guessing with brute-force candidates.

Also restructures the modal: useQuery replaces the manual fetch effect +
refs + withRetry, pure helpers (summarize/classify/reconstruct) live in
speedUpUtils.ts, the bitcoin base asset constant replaces the
BTC_ASSET_ID/store-fetched btcAsset pair, and the misleading `assetId`
prop is dropped since the modal is BTC-only by design.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…action UX

Now that unchained returns a BIP44 `path` on every xpub-derived address,
the speed-up modal can:

- Classify each vout deterministically via `addressNList[3] === 1`,
  dropping the `intendedSendSats` amount-match heuristic and its known
  self-send / same-value edge cases.
- Resolve input addressNLists from the account's owned-address map alone,
  removing the redundant `getUtxos` round trip and collapsing
  `buildOwnedAddressNListMap` to a single source.

Also tightens the RBF notification UX:

- New translation for `transactionHistory.replaced` so the replaced action
  reads "You sped up your send of X BTC" instead of the bare word "Replaced",
  which doubled up with the status pill.
- Replaced status pill switches from yellow to gray so it's visually
  distinct from Pending.
- Dispatches the Replaced upsert before the new Pending insert so the
  reducer's auto-stamp orders the new tx above the replaced one.

Adds unit coverage for `classifyOriginalOutputs` and
`reconstructReplacementInputs` (chain-index branches, metadata priority,
multi-vin indexing, empty-array guard, throw paths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight requested a review from a team as a code owner May 26, 2026 16:10
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@kaladinlight, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 18 minutes and 19 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0522b782-e910-4c9f-a167-79089e30861d

📥 Commits

Reviewing files that changed from the base of the PR and between 902079c and cd9fa26.

📒 Files selected for processing (5)
  • packages/chain-adapters/src/utxo/types.ts
  • src/assets/translations/en/main.json
  • src/components/Layout/Header/ActionCenter/components/SpeedUpModal.tsx
  • src/components/Layout/Header/ActionCenter/components/speedUpUtils.test.ts
  • src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts
📝 Walkthrough

Walkthrough

Refactors the Bitcoin RBF speed-up flow: adds an optional derivation path to addresses, extracts reconstruction utilities/types, moves SpeedUpModal to a react-query-driven reconstruction/mutation flow, updates UI wiring and translations, and expands tests for classification, input reconstruction, and output building.

Changes

Bitcoin RBF Speed-Up Refactoring

Layer / File(s) Summary
Type contracts and imports
packages/chain-adapters/src/utxo/types.ts, src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts
Adds optional path to exported Address; updates imports to include bip32ToAddressNList and introduces summarizeOriginalTx types used by reconstruction utilities.
Reconstruction utilities and output builder
src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts
Adds buildOwnedAddressNListMap, classifyOriginalOutputs, reconstructReplacementInputs, and buildReplacementOutputs to map owned addresses to addressNLists, classify vouts as change/payment, reconstruct inputs with vout/addressNList resolution, and build replacement outputs with dust and OP_RETURN validation.
SpeedUpModal refactor to query-driven flow
src/components/Layout/Header/ActionCenter/components/SpeedUpModal.tsx
Moves reconstruction to a react-query speedUpQuery, derives fee-selection via FEE_MULTIPLIERS, rebuilds/signs/broadcasts replacement BTCSignTx in a mutation, marks original actions as Replaced, upserts new Send actions as Pending, and simplifies props (removes assetId).
Component wiring, UI, and translations
src/components/Layout/Header/ActionCenter/ActionCenter.tsx, src/components/Layout/Header/ActionCenter/components/ActionStatusTag.tsx, src/assets/translations/en/main.json
Stops passing assetId into SpeedUpModal; changes ActionStatus.Replaced tag color to gray; adds speedUp.errors translation strings and updates transactionHistory.replaced to a speed-up specific interpolated message.
Tests
src/components/Layout/Header/ActionCenter/components/speedUpUtils.test.ts
Adds/extends tests covering classifyOriginalOutputs, reconstructReplacementInputs, and buildReplacementOutputs for classification, metadata/owned-map fallbacks, vout resolution, dust-edge cases, OP_RETURN lifting, and expected error keys.

Sequence Diagram

sequenceDiagram
  participant User
  participant SpeedUpModal
  participant speedUpQuery
  participant speedUpMutation
  participant Blockchain
  participant ActionService
  User->>SpeedUpModal: open modal (txHash)
  SpeedUpModal->>speedUpQuery: fetch original tx & reconstruct inputs/outputs
  speedUpQuery-->>SpeedUpModal: OriginalTxSummary + ReconstructedInputs/Outputs
  User->>SpeedUpModal: set fee & confirm
  SpeedUpModal->>speedUpMutation: build & sign replacement BTCSignTx
  speedUpMutation->>Blockchain: broadcast replacement tx
  Blockchain-->>speedUpMutation: replacement txHash
  speedUpMutation->>ActionService: mark original as Replaced
  speedUpMutation->>ActionService: upsert new Send action (Pending)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I dug through types and query-filled code,
I mapped BIP44 paths down the winding road,
Rebuilt the inputs, trimmed dust from the heap,
Replaced with a fee — now the mempool can sleep. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: resolving Bitcoin RBF address paths and refining action UX as evidenced by path resolution changes, output classification updates, modal refactoring, and UX improvements across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/btc-speed-up-address-resolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/Layout/Header/ActionCenter/components/SpeedUpModal.tsx`:
- Around line 308-330: The current code filters out payment outputs without an
address (using .filter(Boolean(o.address))) which drops OP_RETURN/data outputs
and changes tx semantics; instead, preserve all outputs from
classifyOriginalOutputs by removing that filter and map each output to a
BTCSignTxOutputSpend entry: when o.address exists set addressType to
BTCOutputAddressType.Spend and include address/amount as before, and when
o.address is missing preserve the output as a data/return output (include any
script/data fields present on the original output) or set an appropriate enum
value (e.g., BTCOutputAddressType.Return/Data) if available; update the mapping
used to produce spendOutputs (and the analogous mapping at lines ~342-344) so
addressless outputs are passed through to signing rather than dropped.
- Around line 119-131: The useEffect that assigns selectedFeeRate from
speedUpQuery.data?.summary.feeRate should be guarded so it doesn't overwrite a
user-selected bump—only set selectedFeeRate when it is currently undefined/null
(or when a new tx is loaded), e.g. protect the assignment inside the effect with
a check for selectedFeeRate state or a separate "userHasSelected" flag;
reference selectedFeeRate, useEffect, and speedUpQuery. For addressless outputs
make payment/value math and output construction consistent: either reconstruct
and include addressless outputs in outputs (so paymentOutputs and spendOutputs
match) or remove their value from totalPaymentValue/newChangeSats and omit them
from outputs (or detect and block speed-up when addressless outputs exist);
reference paymentOutputs, spendOutputs, outputs, and newChangeSats so the
change/fee computation and final outputs are aligned.

In `@src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts`:
- Around line 261-280: The code currently only uses vin.addresses?.[0]
(vinAddress) when resolving the BIP44 path and throws if addressNList is
missing; change the lookup to fall back to the spent output's address from
prevTx.vout[voutIndex] (e.g. inspect
prevTx.vout[voutIndex].scriptPubKey?.addresses?.[0] or value equivalent) before
failing: keep using metadata?.inputs[index]?.addressNList first, then
vinAddress, then the prev output address, and finally ownedAddressMap.get(...)
to produce addressNList so that path resolution succeeds when the provider
omitted vin addresses; update references around resolveVinVoutIndex, voutIndex,
vinAddress, prevTx.vout[voutIndex], metadata?.inputs[index]?.addressNList, and
ownedAddressMap accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5131bfb4-c6f2-463c-ad17-5a7427a58cba

📥 Commits

Reviewing files that changed from the base of the PR and between 92f8790 and f86978b.

📒 Files selected for processing (7)
  • packages/chain-adapters/src/utxo/types.ts
  • src/assets/translations/en/main.json
  • src/components/Layout/Header/ActionCenter/ActionCenter.tsx
  • src/components/Layout/Header/ActionCenter/components/ActionStatusTag.tsx
  • src/components/Layout/Header/ActionCenter/components/SpeedUpModal.tsx
  • src/components/Layout/Header/ActionCenter/components/speedUpUtils.test.ts
  • src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts
💤 Files with no reviewable changes (1)
  • src/components/Layout/Header/ActionCenter/ActionCenter.tsx

Comment thread src/components/Layout/Header/ActionCenter/components/SpeedUpModal.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts (1)

351-356: 💤 Low value

Type assertion is validated but could be more defensive.

The as string assertion on line 353 is safe because line 324 already returns an error for non-OP_RETURN outputs without addresses. However, for extra safety and self-documentation, consider an explicit guard:

if (!output.address) continue // unreachable after validation, but defensive

This would make the invariant explicit and avoid future refactoring risks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts` around
lines 351 - 356, Add a defensive guard before constructing the
BTCSignTxOutputSpend: explicitly check output.address and skip/continue if falsy
(e.g. if (!output.address) continue) so the creation of spend
(BTCSignTxOutputSpend with addressType BTCOutputAddressType.Spend and pushing
into signOutputs) never relies solely on the prior validation invariant; keep
the rest of the logic (creating spend and signOutputs.push(spend)) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts`:
- Around line 351-356: Add a defensive guard before constructing the
BTCSignTxOutputSpend: explicitly check output.address and skip/continue if falsy
(e.g. if (!output.address) continue) so the creation of spend
(BTCSignTxOutputSpend with addressType BTCOutputAddressType.Spend and pushing
into signOutputs) never relies solely on the prior validation invariant; keep
the rest of the logic (creating spend and signOutputs.push(spend)) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69f9bc72-04b9-40c2-82b1-1a07725fb4c9

📥 Commits

Reviewing files that changed from the base of the PR and between f86978b and 902079c.

📒 Files selected for processing (5)
  • packages/chain-adapters/src/utxo/types.ts
  • src/assets/translations/en/main.json
  • src/components/Layout/Header/ActionCenter/components/SpeedUpModal.tsx
  • src/components/Layout/Header/ActionCenter/components/speedUpUtils.test.ts
  • src/components/Layout/Header/ActionCenter/components/speedUpUtils.ts
💤 Files with no reviewable changes (1)
  • packages/chain-adapters/src/utxo/types.ts

Refactor the replacement-tx output construction to a pure, testable helper
and fix three correctness issues found during review.

- Preserve OP_RETURN data (Thorchain/Relay/ButterSwap memos, send-with-memo)
  by lifting it to `txToSign.opReturnData`. The previous code silently
  dropped addressless outputs while still counting their value, which
  shifted sats into change and broke the swap memo on the replacement.
- Refuse speed-up on multiple change outputs. The previous loop would
  have emitted each change position at the full `newChangeSats` value,
  producing outputs > inputs and an invalid tx.
- Refuse speed-up on non-OP_RETURN addressless outputs (bare multisig,
  non-standard scripts).
- Display the actual fee the user will pay (`actualNewFeeSats`). When new
  change would land below dust, those sats get absorbed into the fee by
  the wallet — the previous UI showed only the targeted fee, so the user
  agreed to one amount and silently paid slightly more.

Output building moves to `buildReplacementOutputs` in speedUpUtils, which
returns either the built `{ outputs, opReturnData }` or `{ error }` with a
translation key the caller passes straight to `translate()`. 12 new unit
tests cover: single change, change-first ordering, OP_RETURN lift,
below-dust drop, exact-dust boundary, negative change, multi-change error,
multi-OP_RETURN error, addressless-non-OP_RETURN error, no-change tx,
amount preservation.

OP_RETURN exact-position preservation would require BTCSignTxOutputMemo
support in hdwallet-native and hdwallet-ledger; both currently only
handle the legacy top-level `opReturnData` field which appends at the
end. For our memo consumers (Thorchain Bifrost, Relay, ButterSwap)
position doesn't matter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kaladinlight kaladinlight force-pushed the fix/btc-speed-up-address-resolution branch from 902079c to 420106f Compare May 26, 2026 18:14
@kaladinlight kaladinlight enabled auto-merge (squash) May 26, 2026 18:14
@kaladinlight kaladinlight merged commit feec60b into develop May 26, 2026
4 checks passed
@kaladinlight kaladinlight deleted the fix/btc-speed-up-address-resolution branch May 26, 2026 18:25
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.

1 participant