Skip to content

feat(sdk-coin-dot): add MPCv2 support to recoverConsolidations - #9486

Open
ralph-bitgo[bot] wants to merge 1 commit into
wci-1227-dot-mpcv2-recoveryfrom
feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations
Open

feat(sdk-coin-dot): add MPCv2 support to recoverConsolidations#9486
ralph-bitgo[bot] wants to merge 1 commit into
wci-1227-dot-mpcv2-recoveryfrom
feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations

Conversation

@ralph-bitgo

@ralph-bitgo ralph-bitgo Bot commented Aug 12, 2026

Copy link
Copy Markdown

What

  • Detect MPCv2 vs MPCv1 once at the top of Dot.recoverConsolidations() via the shared getEddsaSigningMaterial helper (from @bitgo/sdk-core, extracted in WCI-1276), instead of decrypting the keycard on every scanned index.
  • Thread the resolved route into each recover() call via a new multisigTypeVersion: 'MPCv2' option on DotRecoveryOptions, so recover() skips its own per-call detection when the caller already knows the answer.
  • Add MPCv2 signed recovery to Dot.recover() itself (previously only MPCv1/unsigned-sweep paths existed), using the shared signEddsaMpcV2RecoveryTx helper, gated behind a new private isMpcv2SigningMaterial() detector for standalone recover() calls.
  • Add unit tests: MPCv2 signed native DOT consolidation across 2+ funded indexes (base address uses MPCv2-correct derivation), isMpcv2SigningMaterial called exactly zero times inside recover() when multisigTypeVersion is pre-resolved, MPCv1 regression (base address/signing unchanged), and MPCv2 unsigned/cold-path regression (no passphrase → isMpcV2 false).

Follows the pattern already established and merged for SOL (WCI-496, PR #9116).

Why

recoverConsolidations() scans receive-address indexes and sweeps funds to the base address (index 0) via recover(). For MPCv2 wallets this was broken in two ways: the base destination address was derived assuming the legacy MPCv1 signing path, and per-index detection running inside recover() would decrypt the same keycard once per scanned index (wasteful and slow across a 20+ index scan range). This ticket covers the SDK hot signed path only (caller passes walletPassphrase) — WRW always strips walletPassphrase, so WRW consolidation is unaffected. Dot.recover()'s own MPCv2 support is otherwise tracked separately in WCI-1227, but implementing it here was necessary for recoverConsolidations() to have anywhere correct to route MPCv2 signing.

Test plan

  • yarn unit-test in modules/sdk-coin-dot — 211 passing (9 new MPCv2 tests + 2 new guard-clause tests), 0 failing
  • tsc --build — no errors
  • eslint --quiet . — 0 errors

Ticket: WCI-1236

@linear-code

linear-code Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

WCI-1236

@ralph-bitgo
ralph-bitgo Bot force-pushed the feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations branch from 8564bcd to 5a7c848 Compare August 12, 2026 13:43
@vibhavgo
vibhavgo force-pushed the feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations branch 2 times, most recently from 4518235 to ee14577 Compare August 13, 2026 06:41
@vibhavgo
vibhavgo changed the base branch from master to wci-1227-dot-mpcv2-recovery August 13, 2026 18:50
@vibhavgo
vibhavgo force-pushed the feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations branch from ee14577 to a08ca3e Compare August 14, 2026 06:30
@vibhavgo
vibhavgo marked this pull request as ready for review August 14, 2026 06:37
@vibhavgo
vibhavgo requested a review from a team as a code owner August 14, 2026 06:37
@vibhavgo

Copy link
Copy Markdown
Contributor

AI-generated review — auto-generated by Claude Code (wallet-platform:review-pr). Please use your own judgment before acting on any findings.


Phase 1 — Summary

This PR adds MPCv2 support to Dot.recoverConsolidations() by hoisting the getEddsaSigningMaterial call out of the per-index recover() loop and threading the resolved material in via a new precomputedMaterial?: EddsaSigningMaterial parameter on recover(). It also adds the MPCv2 signed path to standalone recover() itself (previously only MPCv1 and unsigned-sweep existed). Two files changed: modules/sdk-coin-dot/src/dot.ts (+101/−34) and modules/sdk-coin-dot/test/unit/dot.ts (+145/−100).

flowchart TD
    A[recoverConsolidations] -->|walletPassphrase && userKey| B[getEddsaSigningMaterial — called ONCE]
    A -->|no passphrase| C[signingMaterial = undefined]
    B --> D{loop over indexes}
    C --> D
    D -->|each index| E[recover(recoverParams, signingMaterial)]
    E -->|precomputedMaterial != null| F[use precomputedMaterial — skip per-call detection]
    E -->|precomputedMaterial == null| G[getEddsaSigningMaterial — standalone path]
    F --> H[addRecoverySignature / signEddsaMpcV2RecoveryTx]
    G --> H
Loading

Phase 2 — Ticket Alignment

Ticket: WCI-1236

PR title: feat(sdk-coin-dot): add MPCv2 support to recoverConsolidations — aligns well. The implementation matches the stated goal (detect-once, thread material, fix base address derivation for MPCv2). One minor discrepancy: the PR body describes a multisigTypeVersion: 'MPCv2' option on DotRecoveryOptions, but the actual implementation uses the cleaner precomputedMaterial second parameter instead. The end result is equivalent or better, but the body/spec diverged from what landed.


Phase 3 — Index Analysis

No new database queries introduced.


Phase 4 — Type Safety

Two double-cast patterns in test code:

basecoin as unknown as { getEddsaSigningMaterial: unknown }

This appears twice (in the recover() and recoverConsolidations() test suites). It's a common sinon pattern for spying on private methods; no production code is affected. No as any, @ts-ignore, or unsafe casts found in dot.ts.

consolidationTransactions: any[] on line ~493 of dot.ts is pre-existing (unchanged by this PR).


Phase 5 — MPCv2 Correctness & Test Quality

Correctness

  • recoverConsolidations() detects signing material ONCE before the loop — signingMaterial is resolved before the for loop and threaded into every recover() call.
  • ✅ Detection guarded on params.walletPassphrase && params.userKey — exact guard in diff; unsigned sweeps correctly pass undefined.
  • recover() short-circuits with precomputedMaterial ?? before calling getEddsaSigningMaterial — correct null-coalescing pattern.
  • assert() guards remain in recover() for the standalone path — assert(params.backupKey, ...) and assert(params.walletPassphrase, ...) are untouched.

Test Quality

  • ✅ Test asserts getEddsaSigningMaterial called exactly once across a multi-index scan — sandBox.assert.calledOnce(getEddsaMaterialSpy) in "should call getEddsaSigningMaterial exactly once across all recover() iterations".
  • ✅ MPCv2 signed consolidation test verifies correct base address via tx1.toJson().to === baseAddr where baseAddr is derived from mpcV2CommonKeyChain via MPCv2-correct derivation (mpc.deriveUnhardened(mpcV2CommonKeyChain, 'm/0')).
  • ✅ MPCv1 regression test for consolidations — "should leave MPCv1 base address derivation and signing unchanged (regression)" covers existing WRW keycard path and checks destination address.
  • ✅ Unsigned sweep consolidation test — "should leave the unsigned (no passphrase) cold path unchanged when keycard is MPCv2".
  • ✅ Spy/stub assertions are specific — calledOnce, notCalled used consistently; no bare truthy checks.

Consistency vs related PRs

  • ✅ Same detection-once pattern as ADA consolidations (feat(sdk-coin-ada): add MPCv2 support to recoverConsolidations #9497) — signingMaterial hoisted before loop, passed as second arg.
  • ⚠️ Minor: the two consolidation tests ("should build MPCv2 signed consolidation recoveries..." and "should call getEddsaSigningMaterial exactly once across all recover() iterations") overlap significantly — both spy the same method, run the same scan range (1–4), and assert calledOnce. Consider collapsing into one or ensuring each tests a distinct concern.

Summary

No blocking issues. The detection-once pattern is implemented correctly and the test suite covers the main paths (MPCv2 signed, MPCv1 regression, unsigned cold path). The small points worth a follow-up:

  1. PR body/spec driftmultisigTypeVersion option mentioned in the body was not implemented; precomputedMaterial parameter was used instead (fine, but body should be updated or noted for reviewers).
  2. Duplicate test coverage — two consolidation tests assert calledOnce on getEddsaSigningMaterial for the same invocation; deduplicate or differentiate.
  3. Double cast in tests (as unknown as { getEddsaSigningMaterial: unknown }) — minor, acceptable for private-method spying, but worth a comment explaining why.

Generated by [wallet-platform:review-pr] via Claude Code

@vibhavgo
vibhavgo force-pushed the wci-1227-dot-mpcv2-recovery branch from 5b6afb7 to 8378a5a Compare August 14, 2026 07:14
@vibhavgo
vibhavgo force-pushed the feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations branch from a08ca3e to 0f0f341 Compare August 14, 2026 07:17
Detect signing material once in recoverConsolidations() and thread it
into each recover() iteration via an optional precomputedMaterial param,
avoiding per-address keycard decryption across the scan range.

Ticket: WCI-1236
@vibhavgo
vibhavgo force-pushed the feat/sdk-coin-dot/WCI-1236-mpcv2-recover-consolidations branch from 0f0f341 to 39fd5ac Compare August 14, 2026 07:54
@vibhavgo
vibhavgo requested a review from Marzooqa August 14, 2026 07:55
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