You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(minter): mint the pending deposits of credited sweeps on a timer - #220
Turns the pending mints of credited sweeps into ckSOL: a mint timer sends one icrc1_transfer per pending mint, with the sweep signature in the memo, and deposit_status then reports Minted with the ledger block index and the minted amount. A successful mint releases the account, so it can queue its next deposit.
Retries lean on the ledger's deduplication: the created_at_time of the transfer is fixed when the pending mint is enqueued and survives upgrades, every retry sends exactly the same arguments, and a Duplicate reply is recorded as a successful mint with the block index it carries. A pending mint that outlives the 24-hour deduplication window, or whose transfer the ledger deterministically rejects, is quarantined instead of retried, since a retry could no longer be deduplicated and a fresh transfer could mint twice. The timer only reschedules itself immediately when a round made progress, so a temporarily unavailable ledger is retried at the timer interval instead of in a hot loop.
Adds timer-driven minting for credited sweep deposits, including deduplicated retries, quarantine handling, status reporting, observability, and integration coverage.
Changes:
Processes pending mints and persists minted/quarantined outcomes.
Exposes minted statuses through APIs, metrics, and dashboard pagination.
Adds unit and integration tests for minting, retries, and account release.
… flow fixture
A deposit flow applies the lifecycle of an automated deposit to the
state one event at a time, queue, sweep, succeed, credit, and hands the
test every value the next stage introduced: the deposit id assigned by
the state, the account, the sweep signature, the amount to mint and the
created_at_time of the pending mint. The mint tests assert on those
values instead of on literals hidden inside a setup helper. Only the
transitions the mint tests need exist so far.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…with the deposit flow
The deposit flow gains the mint of a pending mint, which hands the test
the minted amount and the ledger block index the state recorded. The
two dashboard tests for minted swept deposits assert on those values
instead of recomputing the minted amount and repeating the deposit id
and block index they passed in.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…posit flow
The four state tests of this branch credit a sweep of two deposits
through the flow and name its two pending mints, the one the test mints
or quarantines and its sibling, instead of queueing three deposits by
id and asserting on the ids and the balance as literals. The flow gains
the quarantine of a pending mint, the amount a credited sweep received
and an accessor returning the pending mints of a sweep as an array.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ests of Deposits
The two test modules of this branch for minting and quarantining a
pending mint of the Deposits state machine repeated the queue, sweep,
finalize and credit prelude. A local helper builds a credited sweep
from the mints the test chooses, keeping the amounts and ids it asserts
on visible in the test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tting the whole history
The minted map retains every historical swept deposit, and the dashboard
formatted all of them before keeping one page. Paginate first, so that
rendering a page stays constant in the number of minted deposits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The metric counts individual swept deposits, not sweep transactions: a
ten-deposit sweep increases it by ten.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tation
A quarantined deposit whose ckSOL mint could not be completed may
nevertheless have a landed mint on the ledger, so minting by hand without
first searching the ledger for the memo can double mint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comparing global pending-mint counts misses progress when the
finalization timer credits a sweep while the round's ledger calls are
awaiting: the newly enqueued mints offset the settled ones and the
backlog is not rescheduled immediately. A round made progress exactly
when one of the deposits it selected left the pending mints.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bound was 96, a round-up chosen when the deposit id joined the sweep memo
and pushed the worst case from 72 to 81 bytes. Nothing derived it and the
proptest only checked that an encoding stayed under it, so the 15 unused bytes
were invisible: a ledger deployed from this constant asks for a larger
max_memo_length than any memo needs.
The doc now breaks the 81 bytes down per CBOR item, and a test pins the
constant to the largest sweep memo, so a field added to the memo fails here
instead of at the ledger. Two proptests bracket the 80 bytes most ICRC-1
ledgers are deployed with: every deposit id up to u32::MAX stays within it,
every larger one exceeds it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Queueing a deposit now requires the minter public key to be recorded first, so
the tests that queue through the deposit flow seed it the way the rest of the
suite does, and the replay test feeds the MinterPublicKeyFetched event before
the events it recorded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The link pointed at master, where the file can move or be renamed. It now
points at the commit that last changed it, and at the line holding
TRANSACTION_WINDOW, so the 24-hour window this constant mirrors stays visible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The note claimed that anything below the maximum rejects every mint, which is
only true below the smallest memo. A limit between the two rejects just the
memos that outgrow it, and in practice none, since a sweep memo stays within
80 bytes for every deposit id up to u32::MAX.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Advancing the timer once and then ticking a fixed ten times only happened to
be enough; the helper polls the status until the mint lands. The delay
constant went with the loop, since nothing else drove a timer with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…h a helper
Every test there spelled out queue, sweep and finalize, so each change to one
of those signatures touched all of them. A finalized_sweep helper takes the
swept deposit ids and queues the ids in between, which the sequence invariant
requires, and credited_sweep now builds on it.
The test that credits a sweep which was never finalized keeps its own setup,
since skipping the finalization is the point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t tests
The two balance tests also re-asserted the pending_mints, minted and quarantined
keys, which the mint and quarantine_pending_mint unit tests of Deposits already
cover, so they now assert the balance their names promise and nothing else.
The test storing the credited timestamp is dropped: the credit_sweep unit test
asserts it on the PendingMint, and the replay test below asserts it again and
that the replayed state equals the live one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The quarantined map stored a SweptDeposit, so quarantining a pending mint threw
away the amount it was going to mint and the created_at_time it was sent with:
exactly what a manual resolution needs, recoverable only from the CreditedSweep
event. It also left the two causes indistinguishable, although they ask opposite
questions of an operator - a sweep that could not be read credited nothing,
while an abandoned pending mint was credited and may already have minted.
Entries now carry a cause, and the cause carries the data that only exists for
it, so a mismatching sweep cannot claim an amount owed. Both quarantine paths
already hold what their cause needs, in the live and the replay path alike, so
no event changes.
The public status keeps reporting the sweep signature alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The table showed only the planned amount, which is the sweepable amount before
the deposit's share of the sweep fee, so for a quarantined pending mint it was
never the amount that should be minted. It now carries the cause and, for a
mint that may not have landed, the amount the deposit is still owed, while a
sweep that could not be read shows no mint because it credited nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One gauge over a mixed map could not distinguish a sweep that credited nothing
from a mint that may already have landed, so an alert could not tell whether
funds are owed. The gauge now carries a cause label and always reports both
series, so a rule can fire on either without waiting for the series to appear.
The count comes from an exhaustive match rather than a label accessor, so a new
cause fails to compile here instead of quietly adding a series nothing alerts on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Manual-recovery and retry documentation currently conflicts with the implemented behavior and could lead operators to double mint.
Review effort: Balanced Findings: None
Previously missed (2)
In code that hasn't changed since last review
Distinguish transient failures from definitive mint rejection
docs/design.md:366
This contradicts both the implementation and the PR contract: only transient call/ledger failures stay queued, while TooOld, BadFee, BadBurn, and InsufficientFunds quarantine the mint immediately. Distinguish transient failures from definitive rejection so the design does not promise retries that never occur.
Require both memo fields before manual minting
minter/src/state/event.rs:219
This manual-resolution instruction is unsafe for a multi-deposit sweep: matching only the sweep signature can find a sibling deposit's mint. The sweep is already credited at this stage, so require matching both memo fields before minting by hand rather than telling operators to credit it again.
The init args took max_memo_length from the largest memo the minter can produce,
so the test ledger accepted one byte more than the 80 bytes the ckSOL ledger is
deployed with, and the suite could not have caught a memo that only fits in the
larger limit. It now sets 80 of its own, and the constant goes back to
describing the encoding rather than prescribing a deployment.
A deposit id beyond u32::MAX is the only memo that does not fit, which a ledger
upgrade can make room for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… blocks
advance_time_and_settle waited out the wall clock, which only lets a round run
on an instance that produces its own blocks. The PocketIC instances behind the
mocked tests do not, so wait_for_deposit_minted advanced half an hour of their
time without executing anything and timed out, while sleeping a full minute of
real time doing it.
It now ticks those instances instead, the way the test it replaced did, and
keeps waiting out the outcalls of a live one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
The new historical mint dashboard remains unbounded in pagination cost as completed deposits accumulate.
Review effort: Balanced Findings: None
Previously missed (2)
In code that hasn't changed since last review
Dashboard pagination scans and renders unbounded historical pages
minter/src/dashboard/mod.rs:145
from_rows still does not keep dashboard rendering constant as the historical mint map grows: skip(offset) walks every preceding BTreeMap entry on deep pages, and DashboardTablePagination::new materializes (and the template renders) one link per page even on page 1. Because minted() retains every completed deposit, dashboard cost remains unbounded. Use key/cursor pagination plus bounded page controls instead of offset scanning and an all-pages list.
Document all causes of mint deduplication-window retry variants
minter/src/state/deposits/mod.rs:433
This variant is also used when the ledger immediately returns BadFee, BadBurn, or InsufficientFunds (deposit/sweep/mint/mod.rs:126-135), so it does not always mean the mint fell out of the deduplication window. Document both reasons; otherwise maintainers may misdiagnose a deterministic ledger/configuration rejection as an expired retry.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns the pending mints of credited sweeps into ckSOL: a mint timer sends one
icrc1_transferper pending mint, with the sweep signature in the memo, anddeposit_statusthen reportsMintedwith the ledger block index and the minted amount. A successful mint releases the account, so it can queue its next deposit.Retries lean on the ledger's deduplication: the
created_at_timeof the transfer is fixed when the pending mint is enqueued and survives upgrades, every retry sends exactly the same arguments, and aDuplicatereply is recorded as a successful mint with the block index it carries. A pending mint that outlives the 24-hour deduplication window, or whose transfer the ledger deterministically rejects, is quarantined instead of retried, since a retry could no longer be deduplicated and a fresh transfer could mint twice. The timer only reschedules itself immediately when a round made progress, so a temporarily unavailable ledger is retried at the timer interval instead of in a hot loop.🤖 Generated with Claude Code