mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-20 08:48:39 +02:00
feat(restore): operator-configurable wallet restore policy
How far back a wallet may ask THIS instance to scan on import is an operator decision — the backfill cost lands on its LWS/node. Add WALLET_RESTORE_POLICY (create-only | bounded | unlimited, default bounded) + WALLET_MAX_RESTORE_DEPTH_DAYS, advertise it via /capabilities so the wallet adapts its import UX, and enforce it at the scan path (xmr/wow register, grin scan): a too-deep start_height is a ValidationError naming the bound, never the caller's value. Create needs no check (no tip lookup). Self-sovereignty stays intact at the ecosystem level — a deeper restore means self-hosting or a looser operator. Fail-closed config validation; a unit test per policy; OpenAPI drift clean.
This commit is contained in:
@@ -49,6 +49,17 @@ pub struct FeatureCapabilities {
|
||||
pub tips: bool,
|
||||
}
|
||||
|
||||
/// This instance's wallet-restore (import) policy. The wallet uses it to adapt
|
||||
/// its import UX — hide/grey the restore-height field under `create-only`, warn
|
||||
/// when a chosen date exceeds the bound. `max_depth_days` is present only for
|
||||
/// the `bounded` policy.
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct RestoreCapability {
|
||||
/// `create-only` | `bounded` | `unlimited`.
|
||||
pub policy: String,
|
||||
pub max_depth_days: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, utoipa::ToSchema)]
|
||||
pub struct CapabilitiesResponse {
|
||||
/// Backend version (Cargo package version).
|
||||
@@ -57,6 +68,8 @@ pub struct CapabilitiesResponse {
|
||||
pub contract_version: u32,
|
||||
pub chains: ChainCapabilities,
|
||||
pub features: FeatureCapabilities,
|
||||
/// Wallet restore (import) policy for this instance.
|
||||
pub restore: RestoreCapability,
|
||||
}
|
||||
|
||||
/// Whether an enabled chain can actually be served (its infra secret/URL is
|
||||
@@ -115,6 +128,13 @@ pub fn effective_capabilities(config: &Config) -> CapabilitiesResponse {
|
||||
&& config.identity.public_api_url.is_some(),
|
||||
tips: config.features.tips,
|
||||
},
|
||||
restore: RestoreCapability {
|
||||
policy: config.restore.policy.as_str().to_string(),
|
||||
max_depth_days: match config.restore.policy {
|
||||
crate::config::RestorePolicy::Bounded => Some(config.restore.max_depth_days),
|
||||
_ => None,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ use utoipa::OpenApi;
|
||||
crate::api::capabilities::ChainCapabilities,
|
||||
crate::api::capabilities::ChainCapability,
|
||||
crate::api::capabilities::FeatureCapabilities,
|
||||
crate::api::capabilities::RestoreCapability,
|
||||
crate::api::prices::PricesResponse,
|
||||
crate::api::auth::AuthResponse,
|
||||
crate::api::auth::UserInfo,
|
||||
|
||||
@@ -124,7 +124,13 @@ pub async fn scan(
|
||||
extract_user_id_from_token(&state, &headers).await?;
|
||||
validate_hex64(&req.rewind_hash, "rewind_hash")?;
|
||||
|
||||
let view = grin_client(&state)?
|
||||
let client = grin_client(&state)?;
|
||||
if let Some(h) = req.start_height {
|
||||
// Restore: gate the scan depth against this instance's policy.
|
||||
let tip = client.get_height().await?;
|
||||
state.config.restore.enforce("grin", h, tip)?;
|
||||
}
|
||||
let view = client
|
||||
.scan_rewind_hash(&req.rewind_hash, req.start_height)
|
||||
.await?;
|
||||
Ok(Json(GrinScanResponse {
|
||||
|
||||
@@ -300,6 +300,10 @@ pub async fn register(
|
||||
|
||||
match req.start_height {
|
||||
Some(h) => {
|
||||
// Restore: gate the scan depth against this instance's policy (the
|
||||
// backfill cost lands on our LWS). `None` (create) needs no check.
|
||||
let tip = client.get_blockchain_height().await?;
|
||||
state.config.restore.enforce(&asset, h, tip)?;
|
||||
client
|
||||
.import_account(&req.address, &req.view_key, h)
|
||||
.await?
|
||||
|
||||
Reference in New Issue
Block a user