Skip to content

Feat: Mint operation saga#113

Open
igbopharaoh wants to merge 13 commits intocashubtc:masterfrom
igbopharaoh:mint-operation-saga
Open

Feat: Mint operation saga#113
igbopharaoh wants to merge 13 commits intocashubtc:masterfrom
igbopharaoh:mint-operation-saga

Conversation

@igbopharaoh
Copy link
Contributor

Description

This PR replaces one-shot mint quote redemption with a persisted mint saga (MintOperationService) to make minting crash-safe and recoverable.

Why

Mint redemption previously depended on a single execution path, which left a crash window between successful remote issuance and local proof persistence. This change introduces a stateful operation flow with explicit recovery so retries and restarts are safe.

What changed

  • Added mint saga domain under packages/core/operations/mint:
    • MintOperation state model (init -> prepared -> executing -> finalized | rolled_back)
    • MintMethodHandler abstraction and MintHandlerProvider
    • MintOperationService with init/prepare/execute/redeem/recoverPendingOperations
  • Added bolt11 mint handler:
    • deterministic output creation in prepare
    • ALREADY_ISSUED reconciliation in execute
    • recoverExecuting logic based on remote quote state and restore flow
  • Added crash-safe proof persistence flow:
    • persist outputs from execute result when available
    • fallback restore from deterministic outputData
    • idempotent checks to avoid duplicate writes
  • Integrated saga into app flow:
    • QuotesApi.redeemMintQuote now routes to mintOperationService.redeem
    • MintQuoteService.redeemMintQuote now delegates to mint saga
    • Manager now wires mint handler/provider and runs recoverPendingMintOperations() on startup
  • Added new lifecycle events:
    • mint-op:prepared
    • mint-op:executing
    • mint-op:finalized
    • mint-op:rolled-back
    • existing mint-quote:* events remain intact
  • Added persistence support for mint operations across backends:
    • repository interface in core + memory implementation
    • sqlite3, sqlite-bun, indexeddb, expo-sqlite repository implementations
    • schema/migration updates and exports in each adapter package

Closes #96

@changeset-bot
Copy link

changeset-bot bot commented Mar 13, 2026

🦋 Changeset detected

Latest commit: a40834f

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

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

Copy link

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

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: e2e9be4f89

ℹ️ 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 on lines +332 to +334
const initOp = await this.init(mintUrl, quoteId, 'bolt11', {});
const preparedOp = await this.prepare(initOp.id);
return this.execute(preparedOp.id);

Choose a reason for hiding this comment

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

P1 Badge Guard quote redemption with a quote-level lock

redeem() relies on getOperationByQuote() for deduplication, but the subsequent operation creation is not in the same critical section, so concurrent calls for the same mintUrl/quoteId can both pass the existence check and create separate operations. In that race, one path can end up rolled back with ALREADY_ISSUED even though redemption is expected to be idempotent. Add a per-quote lock (or transactional uniqueness on mint+quote active op) around lookup + create.

Useful? React with 👍 / 👎.

Comment on lines +371 to +374
const preparedOps = await this.mintOperationRepository.getByState('prepared');
for (const op of preparedOps) {
try {
await this.execute(op.id);

Choose a reason for hiding this comment

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

P2 Badge Skip startup recovery for mints no longer trusted

recoverPendingOperations() replays stale prepared operations without re-checking trust, so operations created while a mint was trusted will still execute after that mint is later marked untrusted. Since startup now invokes this recovery flow, this can trigger wallet/mint calls against a mint the current trust policy rejects. Recovery should gate execute/recover paths with isTrustedMint before contacting the mint.

Useful? React with 👍 / 👎.

@igbopharaoh igbopharaoh force-pushed the mint-operation-saga branch from 26373c2 to 5c044b0 Compare March 13, 2026 13:09
Copy link

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

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: 5c044b095a

ℹ️ 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 on lines +284 to +287
if (existing.state === 'init') {
const prepared = await this.prepare(existing.id);
return this.execute(prepared.id);
}

Choose a reason for hiding this comment

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

P2 Badge Recheck trust before resuming existing mint operations

redeem() resumes persisted operations (init/prepared/executing) without re-validating isTrustedMint, so a quote created while a mint was trusted can still trigger prepare/execute/recoverExecuting calls after that mint is later marked untrusted. This bypasses the trust gate that existed before saga delegation and can still contact blocked mints on user-initiated redemption (not just startup recovery), so trust should be enforced before running any non-terminal operation transitions.

Useful? React with 👍 / 👎.

@Egge21M
Copy link
Collaborator

Egge21M commented Mar 13, 2026

I see you were able to resolve merge hell, awesome! Thank you for taking this on, this already looks amazing! I noticed that you did not follow the same API structure that we have for other saga based operations (e.g. prepareBolt11Mint, executeMint methods in the public API). Is that on purpose?

I wonder whether we should merge the operations API refactor first, and than have one for this one as well.

What do you think?

@igbopharaoh
Copy link
Contributor Author

I see you were able to resolve merge hell, awesome! Thank you for taking this on, this already looks amazing! I noticed that you did not follow the same API structure that we have for other saga based operations (e.g. prepareBolt11Mint, executeMint methods in the public API). Is that on purpose?

Oops, I think that is an oversight on my end, I'll push a commit to address it.

I wonder whether we should merge the operations API refactor first, and than have one for this one as well.
What do you think?

I think we can merge that first since reviewing this can take a bit of time and I've not added the public apis

@Egge21M
Copy link
Collaborator

Egge21M commented Mar 16, 2026

Awesome, I have added a placeholder API for the minting ops in #107
If you like, you can stack this PR ontop of that one and wire the Operation to the new API directly.

@igbopharaoh
Copy link
Contributor Author

Awesome, I have added a placeholder API for the minting ops in #107 If you like, you can stack this PR ontop of that one and wire the Operation to the new API directly.

Great, I'll check it out

Copy link

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

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: 590ab6846b

ℹ️ 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".

try {
releaseMintLock = await this.mintScopedLock.acquire(mintUrl);

let existing = await this.getOperationByQuote(mintUrl, quoteId);

Choose a reason for hiding this comment

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

P1 Badge Enforce quote-level uniqueness in persistent storage

redeem() deduplicates by reading getOperationByQuote() and then creating a new operation, but that protection is only guarded by an in-memory mintScopedLock for this process. In multi-instance setups (for example multiple tabs/processes sharing IndexedDB/SQLite), concurrent redeems for the same mintUrl/quoteId can both observe no active op and both insert, producing competing operations with different outputs where one later rolls back as ALREADY_ISSUED. Add a repository-level uniqueness guarantee (or transactional lookup+insert) for active operations per quote.

Useful? React with 👍 / 👎.

Comment on lines +237 to +239
const handler = this.handlerProvider.get(executing.method);
const { wallet } = await this.walletService.getWalletWithActiveKeysetId(executing.mintUrl);
const result = await handler.execute({

Choose a reason for hiding this comment

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

P2 Badge Revalidate trust before executing a prepared mint op

execute() transitions a prepared operation to executing and calls wallet/mint APIs without checking isTrustedMint. If a quote was prepared while trusted and that mint is later marked untrusted, QuotesApi.executeMint() (or any direct execute call) will still contact the mint, bypassing trust policy on this path. Add a trust gate in execute() before network-facing handler calls.

Useful? React with 👍 / 👎.

@Egge21M
Copy link
Collaborator

Egge21M commented Mar 18, 2026

GM! New API structure has been merged. Please wire you Service in the new OpsApi. Then this should be good for a final review and merge

@ye0man ye0man moved this from Backlog to In Progress in coco Mar 18, 2026
@ye0man ye0man added this to the stable v1 milestone Mar 18, 2026
@ye0man ye0man moved this from In Progress to In Review in coco Mar 18, 2026
@igbopharaoh igbopharaoh force-pushed the mint-operation-saga branch from 590ab68 to 51d11b6 Compare March 18, 2026 09:07
@igbopharaoh igbopharaoh marked this pull request as draft March 18, 2026 09:08
Copy link

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

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: 51d11b6c42

ℹ️ 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 on lines +475 to +476
const { wallet } = await this.walletService.getWalletWithActiveKeysetId(executing.mintUrl);
const result = await handler.recoverExecuting({

Choose a reason for hiding this comment

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

P2 Badge Skip executing-op recovery for untrusted mints

recoverPendingOperations() now replays executing mint ops at startup, but recoverExecutingOperation() proceeds straight to wallet/handler calls without checking isTrustedMint. If a mint was trusted when the op started and later marked untrusted, startup recovery will still contact that mint on this path, bypassing the trust policy enforced for new operations. Add the same trust gate used for prepared-op recovery before invoking network-facing executing recovery.

Useful? React with 👍 / 👎.

@igbopharaoh igbopharaoh force-pushed the mint-operation-saga branch from 51d11b6 to 6798c5f Compare March 18, 2026 12:34
@igbopharaoh igbopharaoh force-pushed the mint-operation-saga branch from 6798c5f to 6797fb6 Compare March 18, 2026 12:46
@igbopharaoh igbopharaoh marked this pull request as ready for review March 18, 2026 12:49
@igbopharaoh
Copy link
Contributor Author

Hello @Egge21M wired the new service into the MintOpsApi, kindly review

Copy link

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

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: 6797fb6075

ℹ️ 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 on lines +380 to +384
this.logger?.info('Mint quote already in ISSUED state, skipping redeem', {
mintUrl,
quoteId,
});
return null;

Choose a reason for hiding this comment

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

P1 Badge Recover persisted outputs before skipping ISSUED redeem

The early return in redeem() when quote.state === 'ISSUED' can strand funds after partial saga failure: if a prior operation for this quote rolled back because proof persistence/recovery failed, and the quote state was later updated to ISSUED (for example by remote state sync), retries hit this branch and never attempt recovery from the operation’s stored outputData. In that state there is no remaining public path to re-run proof recovery for the quote, even though operation metadata still exists.

Useful? React with 👍 / 👎.

Copy link

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

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: c8ad95f208

ℹ️ 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".

Comment on lines +62 to +66
async prepare(input: PrepareMintInput<TSupported>): Promise<PendingMintOperation> {
const initOperation = await this.mintOperationService.init(
input.mintUrl,
input.quoteId,
input.method,

Choose a reason for hiding this comment

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

P1 Badge Reuse the existing quote saga before creating a new prepare op

manager.ops.mint.prepare() always goes straight into init() for the supplied mintUrl/quoteId, unlike redeem() which first looks up an existing operation for that quote. That means a retry, double-click, or a race with the default background auto-redeem path can persist a second mint operation for the same quote; if the first one already issued, the new op generates fresh outputData that cannot recover the original proofs and will later end in ALREADY_ISSUED/error instead of resuming the existing saga.

Useful? React with 👍 / 👎.

Comment on lines +435 to +439
const pendingOps = await this.mintOperationRepository.getByState('pending');
for (const op of pendingOps) {
try {
if (await this.mintService.isTrustedMint(op.mintUrl)) {
await this.checkPendingOperation(op.id);

Choose a reason for hiding this comment

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

P2 Badge Sweep executing mint ops before pending ones during recovery

recoverPendingOperations() currently reconciles pending ops before executing ones, but recoverExecutingOperation() can downgrade an executing op back to pending via transitionToPending() when it cannot finish yet. In startup/manual recovery runs where no processor is draining paid quotes, those downgraded ops miss the pending pass entirely and remain stuck until a second recovery run or explicit user action.

Useful? React with 👍 / 👎.

Copy link

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

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: 10e22d7a70

ℹ️ 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".

Comment on lines +828 to +831
const handler = this.handlerProvider.get(op.method);
const { wallet } = await this.walletService.getWalletWithActiveKeysetId(op.mintUrl);

const result = await handler.checkPending({

Choose a reason for hiding this comment

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

P2 Badge Gate pending payment checks on current mint trust

recoverPendingOperations() already skips untrusted mints, but this method does not. manager.ops.mint.checkPayment() and manager.ops.mint.refresh() both route through here, so a quote that was prepared while a mint was trusted will still call checkMintQuoteState (and can even fall through to execute()) after the user marks that mint untrusted. Add the same isTrustedMint guard here so these inspection APIs do not bypass the trust policy.

Useful? React with 👍 / 👎.

Comment on lines +137 to +141
} else if (remoteQuote.state === 'UNPAID') {
return {
status: 'PENDING',
error: `Recovered: quote ${quoteId} is still UNPAID`,
};

Choose a reason for hiding this comment

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

P2 Badge Treat expired UNPAID quotes as terminal during recovery

If mintProofsBolt11() fails with the mint’s “quote expired” error, execute() recovers through this branch. For Cashu quotes the expiry is carried separately from state, so an expired quote can still come back as UNPAID; returning PENDING here sends the operation back to the pending queue forever, and every retry/startup recovery will loop the same way instead of surfacing a terminal failure. Check remoteQuote.expiry before downgrading an UNPAID quote to pending.

Useful? React with 👍 / 👎.

Copy link

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

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: b37541392d

ℹ️ 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".

Comment on lines +55 to +56
async redeemMintQuote(mintUrl: string, quoteId: string): Promise<void> {
this.logger?.info('Redeeming mint quote', { mintUrl, quoteId });

const trusted = await this.mintService.isTrustedMint(mintUrl);
if (!trusted) {
throw new UnknownMintError(`Mint ${mintUrl} is not trusted`);
}

try {
const quote = await this.mintQuoteRepo.getMintQuote(mintUrl, quoteId);
if (!quote) {
this.logger?.warn('Mint quote not found', { mintUrl, quoteId });
throw new Error('Quote not found');
}
if (!quote.amount) {
this.logger?.warn('Mint quote had undefined amount', { mintUrl, quoteId });
throw new Error('Quote amount undefined');
}
const { wallet } = await this.walletService.getWalletWithActiveKeysetId(mintUrl);
const { keep } = await this.proofService.createOutputsAndIncrementCounters(mintUrl, {
keep: quote.amount,
send: 0,
});
const proofs = await wallet.mintProofsBolt11(quote.amount, quote.quote, undefined, { type: 'custom', data: keep });
await this.eventBus.emit('mint-quote:redeemed', { mintUrl, quoteId, quote });
this.logger?.info('Mint quote redeemed, proofs minted', {
mintUrl,
quoteId,
amount: quote.amount,
proofs: proofs.length,
});
await this.setMintQuoteState(mintUrl, quoteId, 'ISSUED');
await this.proofService.saveProofs(mintUrl, mapProofToCoreProof(mintUrl, 'ready', proofs));
this.logger?.debug('Proofs saved to repository', { mintUrl, count: proofs.length });
} catch (err) {
this.logger?.error('Failed to redeem mint quote', { mintUrl, quoteId, err });
throw err;
}
await this.mintOperationService.redeem(mintUrl, quoteId);

Choose a reason for hiding this comment

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

P1 Badge Track legacy quotes before delegating redemption

When a quote comes from createMintQuote(), addExistingMintQuotes(), or requeuePaidMintQuotes(), there is no mint operation yet—those paths only populate MintQuoteRepository. mintOperationService.redeem() returns null in that case, so this method now resolves successfully without minting proofs or surfacing an error, and MintQuoteProcessor will drop the PAID quote as if redemption succeeded. That breaks all legacy/service-driven mint redemption unless the caller manually created a saga first.

Useful? React with 👍 / 👎.

Comment on lines +281 to +283
if (failure) {
if (initOp) {
await this.tryRecoverInitOperation(initOp);

Choose a reason for hiding this comment

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

P2 Badge Preserve init ops when prepare may have created a quote

MintBolt11Handler.prepare() creates the remote quote before generating outputs, so failures from output generation or the first update() can leave a live quote behind even though prepareNewQuote() throws. This cleanup deletes the only persisted record in that case, which means the caller never learns the quote ID and startup recovery has nothing left to reconcile. That reintroduces a quote-loss window in the crash-safe flow this saga is supposed to provide.

Useful? React with 👍 / 👎.

Comment on lines +932 to +938
const observedPending: PendingMintOperation = {
...op,
lastObservedRemoteState: result.observedRemoteState,
lastObservedRemoteStateAt: result.observedRemoteStateAt,
updatedAt: Date.now(),
};
await this.mintOperationRepository.update(observedPending);

Choose a reason for hiding this comment

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

P2 Badge Serialize pending checks with other state transitions

This method reads a pending snapshot, performs network I/O, and then blindly writes observedPending back to the repository without taking the per-operation lock. If execute()/finalize() runs on the same operation while handler.checkPending() is in flight, this stale write can revert an executing or finalized row back to pending, leading to duplicate issuance attempts or a stuck saga. manager.ops.mint.checkPayment(), refresh(), and startup recovery all reach this path, so it needs the same lock or a compare-and-swap style update.

Useful? React with 👍 / 👎.

Copy link

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

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: c800ae99ba

ℹ️ 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".

'coco-cashu-sqlite-bun': patch
'coco-cashu-indexeddb': patch
'coco-cashu-sqlite3': patch
'coco-cashu-core': patch

Choose a reason for hiding this comment

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

P1 Badge Release the mint-quote API removals as a major change

This changeset still publishes coco-cashu-core as a patch even though the diff removes public manager.quotes.* mint methods, renames the watcher/processor config keys and manager helpers, and drops the mint-quote:* event surface. Consumers on a normal ^ range will pick up this patch automatically and hit compile/runtime breaks without any semver warning, so this needs a breaking-version release or compatibility shims.

Useful? React with 👍 / 👎.

async prepare(
ctx: PrepareContext<'bolt11'>,
): Promise<PendingMintOperation<'bolt11'> & MintMethodMeta<'bolt11'>> {
const quote = ctx.importedQuote ?? (await ctx.wallet.createMintQuoteBolt11(ctx.operation.amount));

Choose a reason for hiding this comment

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

P2 Badge Honor the requested unit when creating bolt11 mint quotes

manager.ops.mint.prepare() now accepts unit, but the bolt11 handler always creates the remote quote with createMintQuoteBolt11(ctx.operation.amount) and only afterwards rejects any unit mismatch. In practice, any caller that sets unit !== 'sat' gets a live quote plus an immediate error, because this path never asks the mint for a quote in the requested unit.

Useful? React with 👍 / 👎.

Comment on lines +193 to +197
case 'UNPAID':
return {
observedRemoteState: quote.state,
observedRemoteStateAt,
category: 'waiting',

Choose a reason for hiding this comment

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

P2 Badge Fail expired UNPAID mint quotes during pending checks

Cashu quote expiry is carried separately from state, but this branch treats every UNPAID quote as still waiting. That leaves recoverPendingOperations() and manager.ops.mint.checkPayment()/refresh() stuck retrying expired quotes forever instead of moving the operation to a terminal failure once quote.expiry has passed.

Useful? React with 👍 / 👎.

Copy link

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

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: f2a13657b3

ℹ️ 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".

});
}

return await this.finalizeIssuedOperation(executing, error);

Choose a reason for hiding this comment

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

P1 Badge Keep unrecoverable ALREADY_ISSUED mints out of finalized

If handler.execute() reports ALREADY_ISSUED after a local persistence/recovery problem, ensureOutputsSaved() can still return false here. This path still calls finalizeIssuedOperation(), which stores the saga as finalized and emits mint-op:finalized even though no proofs were recovered. In that scenario the wallet treats the mint as complete and there is no public retry path left to reconstruct the missing outputs.

Useful? React with 👍 / 👎.

} catch (err) {
this.logger?.error('Failed to update quote state', { mintUrl, quoteId, state, err });
}
this.logger?.error('Failed to process mint operation', { mintUrl, operationId, err });

Choose a reason for hiding this comment

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

P2 Badge Re-enqueue ops that bounce back to pending during finalize

In the new saga flow, mintOperations.finalize() can legitimately leave an operation in pending again via transitionToPending() (for example when recoverExecutingOperation() cannot restore issued proofs yet). Those cases throw a plain Error, not NetworkError; this catch-all branch only logs and drops the queue item, so a PAID mint operation stops being retried by the processor until some manual refresh or startup recovery happens.

Useful? React with 👍 / 👎.

Comment on lines +541 to +545
if (!trusted) {
this.logger.debug('Skipping legacy mint quote reconciliation for untrusted mint', {
mintUrl: quote.mintUrl,
quoteId: quote.quote,
});

Choose a reason for hiding this comment

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

P2 Badge Reconcile skipped legacy quotes after a mint becomes trusted

I checked rg "reconcileLegacyMintQuotes\(" and this helper is only invoked from initializeCoco(). Once this branch skips a legacy quote because the mint is untrusted, nothing re-imports that row when the user trusts the mint later; the rest of the runtime only processes MintOperationRepository entries. That strands pre-migration pending/paid quotes unless the application explicitly calls the new helper.

Useful? React with 👍 / 👎.

Copy link

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

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: 259d7b0142

ℹ️ 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".

Comment on lines +189 to +191
if (!quote.amount || quote.amount <= 0) {
throw new ProofValidationError(`Mint quote ${quote.quote} has invalid amount`);
}

Choose a reason for hiding this comment

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

P2 Badge Reject already-issued quotes in importQuote()

If a caller passes an ISSUED quote snapshot to manager.ops.mint.importQuote(), this path still creates a fresh pending operation and later generates new outputData for it. That output data can never match proofs that were already issued elsewhere, so the eventual ALREADY_ISSUED path cannot recover funds and the import ends in an unrecoverable error instead of failing fast. This is a new public API surface, so it should reject terminal quote states before persisting the init op.

Useful? React with 👍 / 👎.

Comment on lines +35 to +39
this.eventBus.on('mint-op:pending', ({ mintUrl, operation }) => {
if (operation.state !== 'pending') return;
this.handleMintOperationPending(mintUrl, operation as PendingMintOperation);
});
this.eventBus.on('mint-quote:created', ({ mintUrl, quoteId, quote }) => {
this.handleMintQuoteCreated(mintUrl, quoteId, quote);
});
this.eventBus.on('mint-quote:added', ({ mintUrl, quoteId, quote }) => {
this.handleMintQuoteAdded(mintUrl, quoteId, quote);
this.eventBus.on('mint-op:quote-state-changed', ({ mintUrl, operationId, quoteId, state }) => {

Choose a reason for hiding this comment

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

P2 Badge Emit history updates for failed mint operations

When a mint operation expires or otherwise transitions to failed, MintOperationService.failOperation()/failPendingOperation() only emit mint-op:finalized. HistoryService subscribes to mint-op:pending and mint-op:quote-state-changed only, so the existing mint history entry remains stuck at its last quote state (UNPAID/PAID) and no history:updated event is sent for the failure. Any UI or client code that relies on history to show terminal mint outcomes will miss these errors entirely.

Useful? React with 👍 / 👎.

Copy link

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

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: b31621a5d9

ℹ️ 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".

Comment on lines +711 to +715
await this.eventBus.emit('mint-op:quote-state-changed', {
mintUrl: current.mintUrl,
operationId: current.id,
operation: current,
quoteId: current.quoteId,

Choose a reason for hiding this comment

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

P2 Badge Persist finalized mint op before emitting ISSUED

If mintOperationRepository.update(finalized) fails here (for example, a transient IndexedDB/SQLite write error), the code has already broadcast mint-op:quote-state-changed with ISSUED. MintOperationWatcherService stops watching on that event and HistoryService records the mint as issued, but the operation row is still executing, so the processor falls back to generic error handling and drops the op until a manual refresh or next startup recovery. Emitting the ISSUED notification only after the finalized row is stored avoids leaving the saga untracked in that failure mode.

Useful? React with 👍 / 👎.

@Egge21M Egge21M force-pushed the mint-operation-saga branch from b31621a to 151b547 Compare March 20, 2026 11:48
Copy link

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

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: 151b5476ed

ℹ️ 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".

Comment on lines +89 to 93
'mint-op:quote-state-changed',
async ({ mintUrl, operationId, operation, state }) => {
if (state === 'PAID') {
this.enqueue(mintUrl, quoteId, 'bolt11'); // Default to bolt11 for now
this.enqueue(mintUrl, operationId, operation.method);
}

Choose a reason for hiding this comment

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

P2 Badge Enqueue ISSUED quote updates for finalization

When the first remote update a pending mint op sees is ISSUED—for example after reconnecting, after starting the watcher late, or when the subscription backend sends the current state on subscribe—MintOperationWatcherService still emits mint-op:quote-state-changed and then stops watching that quote, but this processor only enqueues PAID. In that case the operation stays persisted as pending and proof recovery/finalization never runs until the user manually refreshes or a later startup recovery sweep happens.

Useful? React with 👍 / 👎.

@Egge21M Egge21M force-pushed the mint-operation-saga branch from 151b547 to a40834f Compare March 20, 2026 12:07
Copy link

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

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: a40834fe91

ℹ️ 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".

Comment on lines +531 to +536
const quotes = await this.mintQuoteRepository.getPendingMintQuotes();

for (const quote of quotes) {
if (mintUrl && quote.mintUrl !== mintUrl) continue;
if (quote.state === 'ISSUED') {
skipped.push(quote.quote);

Choose a reason for hiding this comment

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

P1 Badge Reconcile legacy ISSUED mint quotes before skipping them

reconcileLegacyMintQuotes() now only iterates mintQuoteRepository.getPendingMintQuotes() and then skips ISSUED rows, so upgraded wallets never import legacy quotes that were already marked issued. In the pre-saga flow, MintQuoteService.redeemMintQuote() set the quote state to ISSUED before saveProofs(), so a crash in that window leaves an ISSUED row with missing proofs. After this change, Manager.reconcileLegacyMintQuotes() is the only remaining path that reads MintQuoteRepository, which means those pre-existing crash cases become permanently unrecoverable instead of being migrated into the new recovery flow.

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

Status: In Review

Development

Successfully merging this pull request may close these issues.

Add MintOperation saga for crash-safe mint quote redemption

3 participants