feat: metrics for real minter balance#185
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a periodic Solana RPC balance check for the minter’s main account and exports new Prometheus metrics to track the discrepancy between the on-chain (“real”) balance and the minter’s internally tracked balance.
Changes:
- Add a daily timer task (
RefreshRealBalance) that fetches the minter account’s on-chain balance and records the observed discrepancy in state. - Add
last_balance_discrepancytoStateand expose it via metrics (minter_balance_discrepancy_*). - Add an RPC helper (
get_balance) to query Solana balance via the SOL RPC canister.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
minter/src/state/tests.rs |
Updates state construction expectations to include last_balance_discrepancy. |
minter/src/state/mod.rs |
Adds discrepancy observation storage/accessors and a new timer TaskType. |
minter/src/rpc/mod.rs |
Adds get_balance RPC wrapper and corresponding error type. |
minter/src/metrics.rs |
Exposes new discrepancy and refresh timestamp gauges. |
minter/src/main.rs |
Registers a new daily timer to run the refresh task. |
minter/src/lib.rs |
Exports the new balance_check module. |
minter/src/balance_check.rs |
Implements the daily refresh task that fetches on-chain balance and records discrepancy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| pub async fn refresh_real_balance<R: CanisterRuntime>(runtime: R) { | ||
| let _guard = match TimerGuard::new(TaskType::RefreshRealBalance) { | ||
| Ok(guard) => guard, | ||
| Err(_) => return, | ||
| }; | ||
|
|
||
| let master_key = lazy_get_schnorr_master_key(&runtime).await; | ||
| let address = minter_address(&master_key, &runtime); | ||
|
|
||
| match get_balance(&runtime, address).await { | ||
| Ok(balance) => { | ||
| let observed_at = runtime.time(); | ||
| let diff = mutate_state(|s| { | ||
| s.record_balance_discrepancy(balance, observed_at); | ||
| s.last_balance_discrepancy() | ||
| .expect("BUG: just-recorded discrepancy must be present") | ||
| .diff_lamports | ||
| }); |
There was a problem hiding this comment.
New behavior here (daily balance RPC + state update) is currently untested. Please add unit tests covering: (1) successful refresh records last_balance_discrepancy using the runtime timestamp, (2) RPC error leaves last_balance_discrepancy unchanged, and (3) TimerGuard prevents concurrent refreshes.
| pub async fn get_balance<R: CanisterRuntime>( | ||
| runtime: &R, | ||
| address: Address, | ||
| ) -> Result<Lamport, GetBalanceError> { | ||
| let client = read_state(|state| state.sol_rpc_client(runtime.inter_canister_call_runtime())); | ||
| let result = client | ||
| .get_balance(GetBalanceParams { | ||
| pubkey: address.into(), | ||
| commitment: Some(CommitmentLevel::Finalized), |
There was a problem hiding this comment.
get_balance is a new RPC wrapper but minter/src/rpc/tests.rs doesn’t cover it yet (other RPC helpers in this module are tested). Please add tests for the Consistent(Ok), Consistent(Err), and Inconsistent cases to ensure error mapping stays correct.
|
|
||
| pub const REFRESH_REAL_BALANCE_DELAY: Duration = Duration::from_secs(24 * 60 * 60); | ||
|
|
||
| /// Refresh the cached on-chain balance of the minter's main account. |
There was a problem hiding this comment.
refresh_real_balance does not actually cache/store the real on-chain balance; it only records the discrepancy (real_balance - tracked_balance) via record_balance_discrepancy. Either adjust this docstring to describe what is stored, or extend the state to also persist the observed real balance if that’s the intended behavior.
| /// Refresh the cached on-chain balance of the minter's main account. | |
| /// Refresh the observed on-chain balance of the minter's main account and | |
| /// record its discrepancy relative to the tracked balance. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// changing afterwards. Refreshed once per day by a timer; `None` until | ||
| /// the first refresh succeeds. |
There was a problem hiding this comment.
The doc comment says this observation is "Refreshed once per day", but setup_timers also triggers an immediate refresh on install/upgrade. Update the comment to reflect the actual refresh schedule (e.g., initial refresh shortly after install/upgrade, then daily).
| /// changing afterwards. Refreshed once per day by a timer; `None` until | |
| /// the first refresh succeeds. | |
| /// changing afterwards. Refreshed shortly after install/upgrade, and then | |
| /// once per day by a timer; `None` until the first refresh succeeds. |
| /// Each call makes an RPC request to the Solana network, so this runs at most | ||
| /// once per day (see [`REFRESH_REAL_BALANCE_DELAY`]). |
There was a problem hiding this comment.
The function docs say this runs "at most once per day", but the canister also calls refresh_real_balance immediately on install/upgrade (in addition to the 24h interval). Please adjust the wording to avoid implying a strict daily maximum.
| /// Each call makes an RPC request to the Solana network, so this runs at most | |
| /// once per day (see [`REFRESH_REAL_BALANCE_DELAY`]). | |
| /// Each call makes an RPC request to the Solana network, so periodic refreshes | |
| /// are scheduled with a 24-hour delay (see [`REFRESH_REAL_BALANCE_DELAY`]). |
| w.encode_gauge( | ||
| "minter_balance_discrepancy_lamports", | ||
| discrepancy_lamports, | ||
| "Difference (real on-chain balance minus tracked balance) of the minter's main account at the time of the last successful refresh, in lamports. Both sides are sampled together; refreshed once per day. Reports 0 until the first refresh succeeds.", |
There was a problem hiding this comment.
The metric help text says the discrepancy is "refreshed once per day", but the implementation also refreshes once shortly after install/upgrade. Consider updating the description so dashboards/users understand the extra initial refresh behavior.
| "Difference (real on-chain balance minus tracked balance) of the minter's main account at the time of the last successful refresh, in lamports. Both sides are sampled together; refreshed once per day. Reports 0 until the first refresh succeeds.", | |
| "Difference (real on-chain balance minus tracked balance) of the minter's main account at the time of the last successful refresh, in lamports. Both sides are sampled together; refreshed shortly after install/upgrade and then once per day. Reports 0 until the first refresh succeeds.", |
No description provided.