Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
163 commits
Select commit Hold shift + click to select a range
cb7544c
docs(design): automatic deposit flow on top of the balance sweep
gregorydemay Oct 13, 2026
2decad6
docs(design): address review remarks on the automatic flow
gregorydemay Oct 13, 2026
d6f9701
docs(design): show the automatic flow diagram first and fix its rende…
gregorydemay Oct 13, 2026
3203fc0
docs(design): non-overlapping scan ticks and the exact fee share in t…
gregorydemay Oct 13, 2026
6c93f3a
docs(design): report the watch status from watch_deposit_address itself
gregorydemay Oct 13, 2026
c05a015
docs(design): clarify retries, single-round scans and drop the unused…
gregorydemay Oct 13, 2026
257b359
docs(design): define the scan schedule of a re-armed deposit address
gregorydemay Oct 13, 2026
4efb77d
docs(design): always snapshot the watchlist and name both queueing paths
gregorydemay Oct 13, 2026
f8a8a29
docs(design): say what happens after the last scan of a watched address
gregorydemay Oct 13, 2026
9cf5264
docs(design): apply current parameters at detection and compare depos…
gregorydemay Oct 13, 2026
46f47a2
docs(design): fix the watch window at 24 hours over the 23.5 hour sch…
gregorydemay Oct 13, 2026
e513dfd
docs(design): mention the automatic flow in the withdrawal delay sent…
gregorydemay Oct 13, 2026
ea186e2
docs(design): add the state diagram of an account in the automatic flow
gregorydemay Oct 13, 2026
4edf1df
docs(design): state replay of the recorded fee precisely and fix the …
gregorydemay Oct 13, 2026
1163210
docs(design): let the scan discard a read whose window closed meanwhile
gregorydemay Oct 13, 2026
0519a29
docs(design): make the re-arm subject to the watchlist capacity
gregorydemay Oct 13, 2026
c658da2
docs(design): align the diagram, the delay sentence and the minimum e…
gregorydemay Oct 14, 2026
790ad8e
docs(design): state the hard cost bound and the deposit_sol reply aft…
gregorydemay Oct 14, 2026
b52f2d7
docs(design): exclude the minter principal from automatic registrations
gregorydemay Oct 14, 2026
4c0715b
docs(design): qualify the re-arm, the concurrency bound, the detectio…
gregorydemay Oct 14, 2026
cb528cb
docs(design): show the full Watching reply and scope the capacity err…
gregorydemay Oct 14, 2026
3ab9a97
docs(design): define the next scan time as a due time and scope the e…
gregorydemay Oct 14, 2026
656b946
docs(design): drop the superseded proposals and name the watchlist me…
gregorydemay Oct 14, 2026
5bd1619
docs(design): fix the scan slots at the start of the schedule instead…
gregorydemay Oct 14, 2026
e0d1e67
docs(design): re-arm a dropped automatic deposit for a fresh window
gregorydemay Oct 15, 2026
3228518
refactor(minter): extract the queueing of a deposit and the in-flight…
gregorydemay Oct 14, 2026
171686f
docs(minter): describe InFlightDeposit by the account it reserves
gregorydemay Oct 14, 2026
5fc9c9c
docs(minter): state the in-flight precondition of queue_deposit
gregorydemay Oct 14, 2026
42a2ab6
test(minter): pin the in-flight variant at every deposit stage
gregorydemay Oct 14, 2026
ba1c2c5
feat(minter): add the automatic deposit fee parameter
gregorydemay Oct 14, 2026
8c5723d
refactor(minter): report the maximum automatic deposit fee in the val…
gregorydemay Oct 14, 2026
0a4ab21
refactor(minter): drop the comment on the private fee bound helper
gregorydemay Oct 14, 2026
a7bcf3b
feat(minter): add a bounded map of expiring entries
gregorydemay Oct 14, 2026
3441685
refactor(minter): wrap the timestamps of the timed sized map in a new…
gregorydemay Oct 14, 2026
fa252d0
refactor(minter): name the expiry index insertion of the timed sized …
gregorydemay Oct 14, 2026
570cb81
feat(minter): add the watchlist of deposit addresses with its scan sc…
gregorydemay Oct 14, 2026
f9aefd0
refactor(minter): convert the watchlist times at the boundary of the …
gregorydemay Oct 14, 2026
9822993
refactor(minter): take and report timestamps as a newtype in the watc…
gregorydemay Oct 14, 2026
5b8b389
refactor(minter): add saturating duration arithmetic to the timestamps
gregorydemay Oct 14, 2026
e0a39df
refactor(minter): schedule the scans from a burst of gaps and a stead…
gregorydemay Oct 14, 2026
0b7b3bb
refactor(minter): make an unscanned address due at the time of the read
gregorydemay Oct 14, 2026
6412cec
refactor(minter): inline the admission of a registration into register
gregorydemay Oct 14, 2026
a2e16fa
refactor(minter): keep the watchlist module private behind re-exports
gregorydemay Oct 14, 2026
bcb3ed3
refactor(minter): hold the watchlist in the deposits
gregorydemay Oct 14, 2026
de4c736
refactor(minter): index the burst gaps with a plain cast of the scan …
gregorydemay Oct 14, 2026
caeb73c
test(minter): drop the watchlist tests that only re-check the timed s…
gregorydemay Oct 14, 2026
826b50d
test(minter): inline the two watchlist builders into their only callers
gregorydemay Oct 14, 2026
134aed6
refactor(minter): fix the scan slots at the start of the schedule ins…
gregorydemay Oct 14, 2026
4824497
feat(minter): add the watch_deposit_address endpoint
gregorydemay Oct 14, 2026
ee22e15
refactor(minter): convert the registration times of watch_deposit_add…
gregorydemay Oct 14, 2026
82c4e0f
feat(minter): take a queued account off the watchlist
gregorydemay Oct 14, 2026
aa77558
test(minter): expect the next slot of the schedule after a scan at it…
gregorydemay Oct 14, 2026
2e75a32
test(minter): report the same registration on a repeated call before …
gregorydemay Oct 14, 2026
ed2a039
refactor(minter): register inside the status match and convert the re…
gregorydemay Oct 14, 2026
8d12662
docs(minter): point the latest deposit id at deposit_sol_status
gregorydemay Oct 14, 2026
110cc47
refactor(minter): reject the anonymous caller of watch_deposit_addres…
gregorydemay Oct 14, 2026
9672797
test(minter): compare the registration quotes with get_minter_info
gregorydemay Oct 14, 2026
4a33b69
test(minter): name the integration tests of the endpoint after the au…
gregorydemay Oct 14, 2026
dde75c0
test(minter): share the watch window in nanoseconds through the integ…
gregorydemay Oct 14, 2026
6230992
refactor(minter): derive Clone for the state and its collections
gregorydemay Oct 15, 2026
b010146
test(minter): cover the latest deposit id of an account
gregorydemay Oct 15, 2026
40a7ffa
test(minter): assert a rejected registration leaves the state untouched
gregorydemay Oct 15, 2026
505eeb0
test(minter): table-drive the in-flight stages reported as detected
gregorydemay Oct 15, 2026
966c23d
test(minter): assert the whole stored registration against the first one
gregorydemay Oct 15, 2026
7ad4b78
feat(minter): record the automatic part of a queued deposit and deduc…
gregorydemay Oct 14, 2026
4e9f34d
test: expect the collected automatic deposit fees after upgrading the…
gregorydemay Oct 14, 2026
155c282
refactor(minter): record only the automatic deposit fee with a queued…
gregorydemay Oct 15, 2026
bb4f815
refactor(minter): name the fee of a queued deposit without the flow t…
gregorydemay Oct 15, 2026
e267b74
refactor(minter): return the credited sweep and type the error of its…
gregorydemay Oct 15, 2026
991fe51
test(minter): cover the fee deduction through the deposit flow
gregorydemay Oct 15, 2026
f28f6f1
feat(minter): read deposit address balances in batches with getMultip…
gregorydemay Oct 14, 2026
2d4ecc9
build: pin the SOL RPC crates to the commit adding getMultipleAccounts
gregorydemay Oct 15, 2026
de73408
refactor(minter): read the deposit address balances with the typed ge…
gregorydemay Oct 15, 2026
0b1e28a
test(integration-tests): run against the SOL RPC canister built from …
gregorydemay Oct 15, 2026
08ee723
test(integration-tests): download the pinned SOL RPC canister instead…
gregorydemay Oct 15, 2026
b1899c0
test(minter): test only the minter's handling of the getMultipleAccou…
gregorydemay Oct 15, 2026
d1b1bf6
feat(minter): scan the watched deposit addresses on a timer and queue…
gregorydemay Oct 14, 2026
c9f61cd
refactor(minter): take the scan time as a newtype
gregorydemay Oct 14, 2026
c462a45
refactor(minter): read the watchlist limits through the state and rep…
gregorydemay Oct 14, 2026
1612a14
test(minter): expect the fixed scan slots in the scan timer tests
gregorydemay Oct 14, 2026
225a5ed
refactor(minter): record the automatic deposit fee alone when a scan …
gregorydemay Oct 15, 2026
5b094c4
refactor(minter): queue a scanned deposit with its fee under the new …
gregorydemay Oct 15, 2026
e4dfc6a
test(minter): read the scanned addresses from the typed getMultipleAc…
gregorydemay Oct 15, 2026
3d503ac
feat(minter): let deposit_sol yield to a deposit queued by a scan dur…
gregorydemay Oct 14, 2026
b95d04f
test(minter): queue the scanned deposit of the yield test with the fe…
gregorydemay Oct 15, 2026
d2ca808
test(minter): expect the scanned deposit of the yield test with its f…
gregorydemay Oct 15, 2026
132bfa6
docs: state that the balance scan uses the typed getMultipleAccounts …
gregorydemay Oct 15, 2026
6995961
feat(minter): re-arm the automatic deposits of a dropped sweep
gregorydemay Oct 14, 2026
c5638ae
refactor(minter): re-arm with the drop time as a newtype
gregorydemay Oct 14, 2026
1094bee
feat(minter): snapshot the watchlist before every upgrade
gregorydemay Oct 14, 2026
f1bfda8
refactor(minter): re-arm through the watchlist of the deposits
gregorydemay Oct 14, 2026
eaffc66
refactor(minter): convert the snapshot times at the event boundary
gregorydemay Oct 14, 2026
2c14619
test(minter): drop the read time from the watchlist iteration in the …
gregorydemay Oct 14, 2026
fe81cf6
refactor(minter): snapshot and restore the watchlist of the deposits
gregorydemay Oct 14, 2026
3fece15
refactor(minter): re-arm a dropped automatic deposit for a fresh window
gregorydemay Oct 15, 2026
57839e0
test: compare the restored registration up to its due time
gregorydemay Oct 14, 2026
5b7036f
refactor(minter): tell a manual deposit apart by its fee under the ne…
gregorydemay Oct 15, 2026
739b7ce
feat(minter): carry the start of the scan schedule in the watchlist s…
gregorydemay Oct 14, 2026
e2b2f21
test(minter): expect the fresh window of the re-armed account after r…
gregorydemay Oct 15, 2026
2100e68
test(minter): replay the recorded log after given events for the snap…
gregorydemay Oct 15, 2026
745e58a
feat(minter): report the automatic deposit flow in the metrics and on…
gregorydemay Oct 14, 2026
b5da4f5
refactor(minter): convert the watchlist times for the metrics and the…
gregorydemay Oct 14, 2026
69da25a
refactor(minter): list the watched addresses as of the dashboard's time
gregorydemay Oct 14, 2026
2ef7726
refactor(minter): drop the read time from the dashboard's watchlist l…
gregorydemay Oct 14, 2026
1e66a7c
test(minter): credit the dashboard's automatic deposit with the fee a…
gregorydemay Oct 15, 2026
cb9e085
refactor(minter): report the automatically queued deposits through th…
gregorydemay Oct 15, 2026
e9fb590
test: sweep watched and manual deposits together on solana-test-valid…
gregorydemay Oct 14, 2026
774ccda
docs(minter): drop the queued-deposit count from the changelog metric…
gregorydemay Oct 15, 2026
abe8d75
Merge branch 'main' into DEFI-2780-07-get-multiple-accounts
mbjorkqvist Oct 27, 2026
8de0d0a
fix(minter): reject a getMultipleAccounts response without one accoun…
mbjorkqvist Oct 27, 2026
d6c7759
test(minter): check the getMultipleAccounts request of a balance read
mbjorkqvist Oct 27, 2026
3352ecb
refactor(minter): leave the getMultipleAccounts response size estimat…
mbjorkqvist Oct 27, 2026
987ea9e
Merge branch 'DEFI-2780-07-get-multiple-accounts' into DEFI-2780-08-s…
mbjorkqvist Oct 27, 2026
31d2976
fix(minter): record a scan at the time its reads are sent
mbjorkqvist Oct 27, 2026
d7febe9
test(minter): cover the guard of a tick and the registration check of…
mbjorkqvist Oct 27, 2026
aaa0560
test(minter): return the deposit a scan queued and minted during the …
mbjorkqvist Oct 27, 2026
bee57d8
test(minter): shorten a test name that rustfmt could not fit on one line
mbjorkqvist Oct 27, 2026
039c606
docs(minter): say which deposit deposit_sol returns when a scan queue…
mbjorkqvist Oct 27, 2026
522423d
docs: leave the response size of a balance scan to the SOL RPC canister
mbjorkqvist Oct 27, 2026
2874774
Merge branch 'DEFI-2780-08-scan-timer' into DEFI-2780-09-rearm-droppe…
mbjorkqvist Oct 30, 2026
a27df5b
refactor(minter): re-arm a dropped sweep's automatic deposits outside…
mbjorkqvist Oct 30, 2026
1b04ccb
docs(minter): document the re-arm of dropped automatic deposits
mbjorkqvist Oct 30, 2026
cd92fe4
docs: say that the re-arm of a dropped sweep is not replayed from the…
mbjorkqvist Oct 30, 2026
abc7749
test(minter): compare the whole replayed state after a re-arm with th…
mbjorkqvist Oct 30, 2026
dda2fc8
docs(minter): clarify which drops go through process_dropped_sweep
mbjorkqvist Oct 30, 2026
562fe59
Merge branch 'DEFI-2780-09-rearm-dropped-sweeps' into DEFI-2780-10-wa…
mbjorkqvist Oct 30, 2026
0c7c1cc
fix(minter): restore the watchlist under the window and capacity of t…
mbjorkqvist Oct 30, 2026
24c527b
fix(minter): report the registration time of a watched address from i…
mbjorkqvist Oct 30, 2026
aa02e48
feat(minter): record no snapshot of an empty watchlist, as the ckETH …
mbjorkqvist Oct 30, 2026
633de57
test(minter): replay the snapshot recorded by pre_upgrade in the watc…
mbjorkqvist Oct 30, 2026
aac4631
docs: name skip_pre_upgrade as the way out of a pre_upgrade that keep…
mbjorkqvist Oct 30, 2026
1f35c9d
build: depend on the SOL RPC release that adds getMultipleAccounts
mbjorkqvist Nov 3, 2026
7f83b64
Merge branch 'DEFI-2780-07-get-multiple-accounts' into DEFI-2780-08-s…
mbjorkqvist Nov 3, 2026
7b13495
Merge branch 'DEFI-2780-08-scan-timer' into DEFI-2780-09-rearm-droppe…
mbjorkqvist Nov 3, 2026
14e35d0
Merge branch 'DEFI-2780-09-rearm-dropped-sweeps' into DEFI-2780-10-wa…
mbjorkqvist Nov 3, 2026
e1ed582
docs: qualify the replayed watchlist equality and what skip_pre_upgra…
mbjorkqvist Nov 4, 2026
d9b2b7d
test(minter): check that a restored expired entry is not due and can …
mbjorkqvist Nov 4, 2026
74e5f24
docs: state the measured size of a full watchlist snapshot
mbjorkqvist Nov 4, 2026
e8d61a6
Merge branch 'DEFI-2780-10-watchlist-snapshot' into DEFI-2780-12-auto…
mbjorkqvist Nov 5, 2026
0cdf3a4
fix(minter): export the collected automatic deposit fees as a counter
mbjorkqvist Nov 5, 2026
1062e18
feat(minter): export the age of the earliest due scan instead of the …
mbjorkqvist Nov 5, 2026
10f7c59
feat(minter): export the capacity of the watchlist
mbjorkqvist Nov 5, 2026
edfdb1c
test(integration): check the watchlist metrics while a scan is due an…
mbjorkqvist Nov 5, 2026
510c84b
fix(minter): list only the live registrations on the dashboard
mbjorkqvist Nov 5, 2026
f03c39f
feat(minter): show the times of the watched addresses as dates, with …
mbjorkqvist Nov 5, 2026
2def85f
Merge remote-tracking branch 'origin/main' into DEFI-2780-10-watchlis…
mbjorkqvist Nov 10, 2026
d8677c9
Merge remote-tracking branch 'origin/DEFI-2780-10-watchlist-snapshot'…
mbjorkqvist Nov 10, 2026
1b1157a
Merge remote-tracking branch 'origin/DEFI-2780-12-automatic-flow-metr…
mbjorkqvist Nov 10, 2026
d35c748
refactor(minter): remove the unused registration time of the oldest w…
mbjorkqvist Nov 10, 2026
a748de8
docs: tie the rollback of an upgrade to the automatic flow being disa…
mbjorkqvist Nov 10, 2026
13700bb
Merge remote-tracking branch 'origin/DEFI-2780-10-watchlist-snapshot'…
mbjorkqvist Nov 10, 2026
60bd931
feat(minter): export the outcomes of re-arming dropped automatic depo…
mbjorkqvist Nov 10, 2026
c960107
feat(minter): export the dropped deposits by origin
mbjorkqvist Nov 10, 2026
76edc53
Merge remote-tracking branch 'origin/DEFI-2780-12-automatic-flow-metr…
mbjorkqvist Nov 10, 2026
eb61e53
Merge remote-tracking branch 'origin/main' into DEFI-2780-12-automati…
mbjorkqvist Nov 10, 2026
5dfaa41
Merge remote-tracking branch 'origin/DEFI-2780-12-automatic-flow-metr…
mbjorkqvist Nov 10, 2026
ed84e0f
fix(minter): span the pagination of the watched addresses across all …
mbjorkqvist Nov 10, 2026
91bdceb
test(integration): mint a detected automatic deposit minus its fee an…
mbjorkqvist Nov 10, 2026
c49e963
Merge remote-tracking branch 'origin/DEFI-2780-12-automatic-flow-metr…
mbjorkqvist Nov 10, 2026
f3e4b38
test(integration): share the sweep and the finalization of a deposit …
mbjorkqvist Nov 10, 2026
e8ca51d
Merge remote-tracking branch 'origin/DEFI-2780-12-automatic-flow-metr…
mbjorkqvist Nov 10, 2026
8019fff
fix(minter): count as failed scans only the registrations a failed re…
mbjorkqvist Nov 10, 2026
1f61b8b
docs: cover the last scan, a dropped automatic deposit and the rollou…
mbjorkqvist Nov 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ solana-transaction-status-client-types = "3.1.8"
tar = "0.4.44"
tempfile = "3.26.0"
thiserror = "2.0.18"
time = "0.3.47"
tokio = "1.49.0"

[profile.release]
Expand Down
16 changes: 16 additions & 0 deletions docs/src/development/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ The fees and minimum amounts follow [Section 3.3 of the design](design.md#33-fee
| `nonce_accounts` | empty | Replace with the addresses created in [Create nonce accounts](#create-nonce-accounts). |
| `automatic_deposit_fee` | unset | Keeps the automatic flow disabled; to be set to 0.002 SOL (2,000,000) once it is complete. |

### Enable the automatic deposit flow

Enable the automatic deposit flow on a running minter in two upgrades. The first installs the version with the flow and leaves `automatic_deposit_fee` unset, so the flow stays disabled: `watch_deposit_address` returns `TemporarilyUnavailable` and the scan timer reads no balance. No automatic deposit is queued and no watchlist snapshot is recorded, so this upgrade can still be rolled back to the previous version. Once the minter runs as expected, a second upgrade sets the fee and enables the flow:

```candid
(
variant {
Upgrade = record {
automatic_deposit_fee = opt (2_000_000 : nat64);
}
},
)
```

From then on, a faulty version has to be fixed forward instead of rolled back, as described in [Section 3.5 of the design](design.md#35-events).

## Ledger

Initialization arguments:
Expand Down
6 changes: 3 additions & 3 deletions docs/src/development/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ stateDiagram-v2
Quarantined: Quarantined, until a minter upgrade resolves the deposit
```

The status is `Watching`, with the end of the window, the number of scans so far, the time of the last scan and the time the next scan is *due*, which is not a promise of when it runs: a scan lands on the next tick at or after that time, so the time lies in the past until then, and a client reads a next scan time in the past as a scan still pending, as long as the account is on the watchlist, and `Detected`, with the *deposit id* that identifies a sweep of the account for its whole life, as defined in [Section 3.1.2](#312-manual-flow), once a deposit is in flight for the account. As in the ckETH minter, there is no separate status endpoint: calling `watch_deposit_address` again is how a client follows a registration. Registering an account that is already watched is idempotent and returns the stored registration without extending its window; an account with a deposit in flight is not registered, since the sweep flow already owns its address, and the call returns `Detected`; and an account whose latest deposit is minted or dropped is registered anew, so that the next deposit to the same address needs nothing but another call. The id of the latest deposit is returned in every case, so that a client coming back hours after a detection can follow the deposit with `deposit_sol_status`. An account whose automatically detected deposit was dropped is reported as `Watching` again when the re-arm described below has put it back on the watchlist.
The status is `Watching`, with the end of the window, the number of scans so far, the time of the last scan and the time the next scan is *due*, which is not a promise of when it runs: a scan lands on the next tick at or after that time, so the time lies in the past until then, and a client reads a next scan time in the past as a scan still pending, as long as the account is on the watchlist, and `Detected`, with the *deposit id* that identifies a sweep of the account for its whole life, as defined in [Section 3.1.2](#312-manual-flow), once a deposit is in flight for the account. As in the ckETH minter, there is no separate status endpoint: calling `watch_deposit_address` again is how a client follows a registration. Registering an account that is already watched is idempotent and returns the stored registration without extending its window; an account with a deposit in flight is not registered, since the sweep flow already owns its address, and the call returns `Detected`; and an account whose latest deposit is minted or dropped is registered anew, so that the next deposit to the same address needs nothing but another call. The id of the latest deposit of the account, if any, is returned in every reply except `TemporarilyUnavailable`, so that a client coming back hours after a detection can follow the deposit with `deposit_sol_status`. An account whose automatically detected deposit was dropped is reported as `Watching` again when the re-arm described below has put it back on the watchlist.

The call does not contact the SOL RPC canister and requires no cycles. It is rejected when the latest deposit of the account is quarantined (`Quarantined`, as for `deposit_sol`), when the call would create a new registration while the watchlist is full of live entries (`TooManyWatchedAddresses`, with the id of the latest deposit of the account, if any; an account already watched or with a deposit in flight needs no slot and gets its status), and with `TemporarilyUnavailable` when the minter's public key is not yet known or when the automatic deposit fee has not been configured. A `deposit_sol` call in progress for the same account does not prevent the registration: if that call queues a deposit, the registration is removed again, and otherwise it stays, which is what the caller wanted in either case. A registration is not recorded in the event log, see [Section 3.5](#35-events).

Expand All @@ -181,7 +181,7 @@ An address whose balance is at or above the minimum deposit amount in force at t

The event log is not a vector: a registration records nothing, so an adversary cannot grow the log for free, and the only events the automatic flow adds to the ones a detected deposit shares with a manual one (the submission, settlement, mint and possible failure or expiry of its sweep) are the `QueuedDeposit` of the detected deposit, which requires at least the minimum deposit amount on chain, and the watchlist snapshot taken once per upgrade, whose size is bounded by the capacity, see [Section 3.5](#35-events). A full watchlist blocks honest registrations, and the manual flow remains available in that case; since filling it is free, the number of live registrations is exposed as a metric so that an alert can fire before the capacity is reached.

**Compatibility.** The automatic flow is added to a ckSOL minter that is deployed on mainnet, so it only extends the interface and the state: one new endpoint; a new optional `automatic_deposit_fee` field in the init and upgrade arguments and in `get_minter_info`, together with the collected fees; two fields of the `QueuedDeposit` event, an optional one carrying the fee of the deposit, absent if none is charged, and one carrying its origin, which events recorded before the automatic flow lack and which then reads as manual, since all of them were queued by `deposit_sol`; a new snapshot event recorded by a new `pre_upgrade` hook; and a new timer. The types of `deposit_sol` and `deposit_sol_status` and the stable memory layout are unchanged, and an event log written before the upgrade replays unchanged.
**Compatibility.** The automatic flow is added to a ckSOL minter that is deployed on mainnet, so it only extends the interface and the state: one new endpoint; a new optional `automatic_deposit_fee` field in the init and upgrade arguments and in `get_minter_info`, together with the collected fees, which `get_minter_info` always reports, zero until a sweep collects some; two fields of the `QueuedDeposit` event, an optional one carrying the fee of the deposit, absent if none is charged, and one carrying its origin, which events recorded before the automatic flow lack and which then reads as manual, since all of them were queued by `deposit_sol`; a new snapshot event recorded by a new `pre_upgrade` hook; and a new timer. The types of `deposit_sol` and `deposit_sol_status` and the stable memory layout are unchanged, and an event log written before the upgrade replays unchanged.

#### 3.1.2. Manual Flow

Expand Down Expand Up @@ -655,7 +655,7 @@ The ckSOL minter exposes the following endpoints:
4. `watch_deposit_address(opt subaccount)`: Registers the deposit address of the caller's account for the automatic flow of [Section 3.1.1](#311-automatic-flow), for free, or reports on an existing registration. Returns the deposit address, the current minimum deposit amount and automatic deposit fee as quotes, the status of the account (`Watching` with the end of the window, the number of scans, the time of the last scan and the time the next scan is due, which may lie in the past until the tick that runs it, or `Detected` with the id of the deposit in flight) and the id of the latest deposit queued for the account, if any. Otherwise, an error is returned: `Quarantined` with the id of the quarantined deposit, `TooManyWatchedAddresses` with the id of the latest deposit queued for the account, if any, or `TemporarilyUnavailable`. The owner is always the caller.
5. `withdraw_sol(opt subaccount, amount, address)`: Burns the given amount of ckSOL from the user's account and transfers the same amount minus a fee in SOL to the given user address. Returns the block index of the burn operation on the ckSOL ledger in case of success. Otherwise, an error is returned.
6. `withdraw_sol_status(block_index)`: Returns the withdrawal status (`NotFound`, `Pending`, `TxSent`, `TxFinalized`) for the withdrawal identified by the given block index.
7. `get_minter_info`: Returns information about the ckSOL minter, specifically the various fees, including the automatic deposit fee once configured, the minimum deposit and withdrawal amounts, the current balance of the ckSOL minter, the automatic deposit fees it has collected, its main address, and the addresses of its durable nonce accounts.
7. `get_minter_info`: Returns information about the ckSOL minter, specifically the various fees, including the automatic deposit fee once configured, the minimum deposit and withdrawal amounts, the current balance of the ckSOL minter, the automatic deposit fees it has collected (always reported, zero until a sweep collects some), its main address, and the addresses of its durable nonce accounts.
8. `get_events(start, length)`: Returns a page of the event log, at most 2000 events per call. This is a debug endpoint; backwards compatibility is not guaranteed for it.

The authoritative interface is the Candid file [`minter/cksol_minter.did`](https://github.com/dfinity/cksol/blob/main/minter/cksol_minter.did).
20 changes: 19 additions & 1 deletion docs/src/usage/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,27 @@ A successful response returns the deposit id, which you can use to track the dep
(variant { Ok = 42 : nat64 })
```

## Register your deposit address for automatic deposits

Instead of calling `deposit_sol` after each transfer, call `watch_deposit_address` to have the minter watch your deposit address for about 24 hours and queue the deposit itself once the balance reaches the minimum deposit amount. The call is free and needs no cycles, so no proxy canister is required; the automatic deposit fee reported by `get_minter_info` (`automatic_deposit_fee` field) is deducted from the minted amount instead. The owner is always your calling identity, so pass only the `subaccount` used when calling `get_deposit_address`:
Comment thread
Copilot marked this conversation as resolved.
Resolved

```sh
icp canister call -e prod cksol_minter watch_deposit_address '(record { subaccount = null })'
```

A successful response returns the deposit address, the minimum deposit amount, the automatic deposit fee, the status of the registration and the id of the latest deposit of your account, if any:

```
(variant { Ok = record { status = variant { Watching = record { valid_until = ...; scan_count = 0; last_scanned_at = null; next_scan_at = opt ... } }; ... } })
```

Call `watch_deposit_address` again to follow the registration: the status becomes `Detected` with the deposit id once the minter has queued the deposit, which you can then track with `deposit_sol_status`. Calling it again after the deposit is minted registers the address anew.

The last scan runs about half an hour before the window ends (`valid_until`), so a transfer finalized after it is only detected once you register again, which is free and possible as soon as the window has closed.

## Check a deposit status

After calling `deposit_sol`, track the deposit using the deposit id returned in the response. The status moves through `Queued` → `Swept` → `Finalized` → `Minted` as the minter sweeps the deposit address and mints ckSOL:
After calling `deposit_sol`, or once `watch_deposit_address` reports a deposit as `Detected`, track the deposit using its deposit id. The status moves through `Queued` → `Swept` → `Finalized` → `Minted` as the minter sweeps the deposit address and mints ckSOL:

```sh
icp canister call -e prod cksol_minter deposit_sol_status '(42 : nat64)' --query
Expand Down
12 changes: 12 additions & 0 deletions docs/src/usage/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ sequenceDiagram
Minter-->>User: Minted { block_index, minted_amount }
```

### Automatic deposits

If you cannot call the minter after the transfer, for example because the SOL comes straight from an exchange, register your deposit address with `watch_deposit_address` instead of calling `deposit_sol`:

1. **Register the deposit address.** Call `watch_deposit_address` on the minter with the same optional subaccount as in step 1; the owner is always your principal. The call is free, requires no cycles and returns the deposit address, the current minimum deposit amount and automatic deposit fee, and the status of the registration, which is `Watching` for about 24 hours with the time of the next scan.

2. **Send SOL.** Transfer SOL to the deposit address within that window, as in step 2. The last scan runs about half an hour before the window ends, so a transfer finalized after it is only detected once you register again, which you can do for free as soon as the window has closed.

3. **Let the minter detect the deposit.** The minter reads the balance of every registered deposit address on a schedule, frequently right after the registration and then hourly. Once the balance reaches the minimum deposit amount, the minter queues the deposit as if `deposit_sol` had been called, and the sweep and mint of step 4 follow; the automatic deposit fee in force at detection is deducted from the minted amount instead of cycles.

4. **Track the deposit.** Call `watch_deposit_address` again: once the deposit is detected, the status is `Detected` with the deposit id, which you can follow with `deposit_sol_status` as in step 5. The id of the latest deposit of your account, if any, is returned in every reply except `TemporarilyUnavailable`. Once the deposit is minted, calling `watch_deposit_address` registers the address anew, so that the next deposit to the same address only needs another call. If the sweep fails or expires instead, `deposit_sol_status` reports `Dropped` and the minter puts the address back on the watchlist for a fresh window, if it has room, so that the funds are detected again under a new deposit id, which `watch_deposit_address` reports as above, without calling `deposit_sol`.

## Withdrawal: ckSOL → SOL

1. **Approve the minter.** Grant the minter an [ICRC-2](https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md) allowance on your ckSOL ledger account.
Expand Down
30 changes: 28 additions & 2 deletions integration_tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use canlog::{Log, LogEntry};
use cksol_types::{
Address, DepositSolArgs, DepositSolError, DepositSolId, DepositSolStatus,
GetDepositAddressArgs, MinterInfo, WatchDepositAddressArgs, WatchDepositAddressError,
WatchDepositAddressOk, WithdrawSolArgs, WithdrawSolError, WithdrawSolOk, WithdrawSolStatus,
WithdrawSolStatusArgs,
WatchDepositAddressOk, WatchDepositAddressStatus, WithdrawSolArgs, WithdrawSolError,
WithdrawSolOk, WithdrawSolStatus, WithdrawSolStatusArgs,
};
use cksol_types_internal::{
MinterArg,
Expand Down Expand Up @@ -603,6 +603,32 @@ impl<'a> CkSolMinter<'a> {
.await
}

/// Polls `watch_deposit_address` on a live instance until the scan tick the caller fired
/// has detected a deposit on the address of the caller's account and returns the id of
/// that deposit. The poll never advances time, since that would also fire the sweep
/// timer on whatever is queued by then.
///
/// # Panics
///
/// Panics if no deposit is detected within the timeout.
pub async fn wait_for_deposit_detected(&self, subaccount: Option<Subaccount>) -> DepositSolId {
const MAX_POLLS: usize = 45;
const POLL_INTERVAL: Duration = Duration::from_secs(1);
for _ in 0..MAX_POLLS {
let registration = self
.watch_deposit_address(subaccount)
.await
.expect("watch_deposit_address should succeed");
if let WatchDepositAddressStatus::Detected { deposit_id } = registration.status {
return deposit_id;
}
tokio::time::sleep(POLL_INTERVAL).await;
}
panic!(
"No deposit was detected on the deposit address of subaccount {subaccount:?} within timeout"
);
}

pub async fn withdraw_sol(
&self,
args: WithdrawSolArgs,
Expand Down
Loading
Loading