Skip to content

feat: add recoverable cross-mint swaps - #383

Draft
igbopharaoh wants to merge 12 commits into
cashubtc:masterfrom
igbopharaoh:feat/mint-swaps
Draft

feat: add recoverable cross-mint swaps#383
igbopharaoh wants to merge 12 commits into
cashubtc:masterfrom
igbopharaoh:feat/mint-swaps

Conversation

@igbopharaoh

@igbopharaoh igbopharaoh commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Problem

Coco exposes mint and melt operations separately, leaving callers to manually coordinate moving value between mints. That flow spans two mints, Lightning settlement, variable source fees, destination issuance, and several ambiguous failure boundaries without one durable recovery authority.

Summary

  • add an exact-receive, one-source/one-destination BOLT11 mint-swap parent operation with bounded source debit accounting
  • require capability/trust preflight and NUT-20 locked destination quotes, with persisted authorization before remote side effects
  • persist parent state, child ownership, retries, attention evidence, and an event outbox across memory, SQL, SQLite, Expo SQLite, and IndexedDB adapters
  • add restart-safe processing, canonical quote reconciliation, deterministic issuance recovery, grouped history, typed events, a race-safe waiter, and React bindings
  • document operation lifecycle, fees, recovery, configuration, and adapter migration requirements

Specification And Tracking

Closes #364
Closes #365
Closes #366
Closes #367
Closes #368
Closes #369
Closes #370
Closes #371
Closes #372
Resolves #163

Compatibility

This is additive for wallet callers, but it expands mandatory repository interfaces and persisted schemas. Custom adapters must implement the mint-swap operation and event-outbox repositories and preserve parent ownership on mint and melt children.

Verification

  • bun run --filter='@cashu/coco-core' test:unit (1,097 passed)
  • bun run --filter='@cashu/coco-react' test (49 passed)
  • bun run typecheck
  • bun run build
  • bun run docs:build
  • git diff --check origin/master...HEAD

Changeset

  • adds .changeset/mint-swaps.md with patch releases for all affected published packages

@changeset-bot

changeset-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: aa1b42d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@cashu/coco-core Patch
@cashu/coco-react Patch
@cashu/coco-adapter-tests Patch
@cashu/coco-sql-storage Patch
@cashu/coco-sqlite Patch
@cashu/coco-sqlite-bun Patch
@cashu/coco-indexeddb Patch
@cashu/coco-expo-sqlite Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@robwoodgate

robwoodgate commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

@igbopharaoh - hopefully this will help you out:

/**
 * Fee to add on top of `amount` so the resulting outputs can later be spent at no cost to the
 * receiver (ie sender pays fees). This is the amount `includeFees` adds.
 *
 * @remarks
 * The fee depends on the output count, taken from the default denomination split of `amount`.
 * Pass `nOutputs` when pricing up custom denomination sets.
 * @param amount The amount the receiver should net after swap fees.
 * @param opts.keysetId Optional `keysetId` to price against (default: the wallet's bound keyset)
 * @param opts.nOutputs Optional Override the output count for custom denoms (default: optimal
 *   split).
 * @returns The fee, zero when the keyset charges no input fees.
 */
getFeesToInclude(amount: AmountLike, opts?: { keysetId?: string; nOutputs?: number }): Amount 

cashubtc/cashu-ts#847

Will be in CTS v4.8.0, will release end of the first week of August.

@igbopharaoh

Copy link
Copy Markdown
Contributor Author

Thanks @robwoodgate

@igbopharaoh
igbopharaoh marked this pull request as ready for review July 27, 2026 21:32
@igbopharaoh

Copy link
Copy Markdown
Contributor Author

@codex kindly 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: 82e4600e3d

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


const TERMINAL_STATES = new Set<MintSwapOperationState>(['completed', 'cancelled', 'failed']);
const AUTOMATIC_STATES = new Set<MintSwapOperationState>([
'preparing',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Exclude live preparations from recovery sweeps

With the default processor enabled, every newly created parent enters preparing with no nextAttemptAt, so MintSwapOperationProcessor considers it due on the next five-second sweep. Since prepare() does not hold the operation lock while performing its multiple remote quote calls, a preparation lasting longer than one sweep can concurrently run recoverPreparing(), which marks the parent failed; the in-progress preparation then cannot transition it to prepared. Distinguish stale preparations from active ones or protect preparation with a lock/lease before sweeping this state.

Useful? React with 👍 / 👎.

.slice(offset, offset + limit);
}
const [children, parents] = await Promise.all([
this.historyRepository.getPaginatedHistoryEntries(10_000, 0),

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 Preserve pagination beyond the first 10,000 history rows

Whenever a mint-swap repository is configured—which Manager now always does—history pagination first truncates ordinary history to 10,000 rows and only then applies the requested offset and limit. For wallets with more than 10,000 entries, older records are therefore unreachable (for example, an offset of 10,000 returns no ordinary entries), and merged pages near that boundary are incomplete. The merge needs repository-level pagination or an unbounded/iterative fetch rather than this fixed cap.

Useful? React with 👍 / 👎.

Comment thread .changeset/mint-swaps.md
@@ -0,0 +1,13 @@
---
'@cashu/coco-core': patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Publish the required repository additions as a breaking release

The core package is marked as a patch even though this change adds two mandatory members to the public Repositories contract. An existing third-party adapter installed against the current 1.0 API will therefore be accepted by a patch upgrade but fail during initializeCoco() when startup recovery calls the missing mintSwapOperationRepository; TypeScript adapters also must change before recompiling. Either preserve compatibility with optional/fallback repositories or release @cashu/coco-core with the appropriate breaking version.

Useful? React with 👍 / 👎.

Comment on lines +246 to +247
if (entry.type === 'mint-swap') {
return entry.sourceMintUrl === filter.mintUrl || entry.destinationMintUrl === filter.mintUrl;

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 Normalize the mint URL before filtering history

The new mintUrl filter compares the caller's string directly with normalized persisted URLs. Inputs that identify the same mint but need normalization, such as https://mint.example/ when storage contains https://mint.example, therefore return no ordinary or mint-swap history even though the rest of the mint-swap API accepts and normalizes those URLs. Normalize filter.mintUrl once before applying these comparisons.

Useful? React with 👍 / 👎.

@Egge21M

Egge21M commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Hey man! This is great and I think the direction is solid. However this PR suffers from a similar issue I ran into when working with Wayfinder. This adds close to 8k lines in over 100 files and spans so many independent concepts that its becoming close to impossible to savely review.

Could you please look into chunking this into independently mergeable slices. If that is not possible, maybe we can find a way to turn this into stacked PRs, however independent slices would be much better

@igbopharaoh

igbopharaoh commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Hello @Egge21M GM.
I've been thinking of how to make this easier to review as I have been trying make some refactors on my end and it is quite a lot. I'll refactor this to follow the independent slice approach so each slice can be reviewed and merged cleanly.

Appreciate the feedback.

@igbopharaoh
igbopharaoh marked this pull request as draft July 29, 2026 09:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment