From b0c0e39712fd36e3d3d91bef2b28ded7caf1a2b0 Mon Sep 17 00:00:00 2001 From: Nordic Operator Date: Sun, 7 Jun 2026 09:37:01 +0200 Subject: [PATCH 1/6] cdk-axum info-page: add Experimental features section (payjoin board, on-chain send, PoR) below NUTs Signed-off-by: Matthew Vuk --- crates/cdk-axum/src/router_handlers.rs | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index c5837393f4..d47c825f8f 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -846,6 +846,30 @@ pub(crate) async fn get_index( supported_features.push((29, "Batched minting")); } + // Build experimental (non-NUT) features list — genuinely-added capabilities, + // detected from the mint's advertised on-chain methods. Kept separate from NUTs. + let has_onchain_mint = mint_methods.iter().any(|m| m == "onchain"); + let has_onchain_melt = melt_methods.iter().any(|m| m == "onchain"); + let mut experimental_features: Vec<(&str, &str)> = Vec::new(); + if has_onchain_mint { + experimental_features.push(( + "Payjoin Board", + "On-chain deposit into ecash via BIP77 payjoin — no Lightning invoice. Co-funded boards obscure the amount.", + )); + } + if has_onchain_melt { + experimental_features.push(( + "On-chain Send", + "Withdraw ecash directly to an on-chain Bitcoin address (Ark offboard).", + )); + } + if has_onchain_mint { + experimental_features.push(( + "Proof of Reserves", + "Reserve attestations via Ark self-spend, independently verifiable against the Ark server's key.", + )); + } + // Avatar fallback letter let avatar_letter = name .chars() @@ -1017,6 +1041,26 @@ pub(crate) async fn get_index( } } + // Experimental features section (genuinely-added, non-NUT capabilities) + @if !experimental_features.is_empty() { + div class="card-section-header has-rule" { "Experimental features" } + div style="padding-top:12px" { + div class="features-grid" { + @for (feat_name, feat_desc) in &experimental_features { + div class="feature" { + div class="feature-dot" style="background:var(--yellow-soft)" { + (maud::PreEscaped(r#""#)) + } + div { + span class="feature-name" { (feat_name) } + div style="font-size:11px;color:var(--text-muted);margin-top:3px;line-height:1.45" { (feat_desc) } + } + } + } + } + } + } + // Contact section @if !contact.is_empty() { div class="card-section-header has-rule" { "Contact" } From 1f9c4eadc0b9c6f2e9d27e3dfdbf922e4e4d5339 Mon Sep 17 00:00:00 2001 From: Nordic Operator Date: Sun, 7 Jun 2026 09:55:12 +0200 Subject: [PATCH 2/6] cdk-axum info-page: full-width experimental rows; simplify payjoin/onchain copy; PoR 'via Bark' Signed-off-by: Matthew Vuk --- crates/cdk-axum/src/router_handlers.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index d47c825f8f..f696b8a3b4 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -854,19 +854,19 @@ pub(crate) async fn get_index( if has_onchain_mint { experimental_features.push(( "Payjoin Board", - "On-chain deposit into ecash via BIP77 payjoin — no Lightning invoice. Co-funded boards obscure the amount.", + "On-chain deposit into ecash via BIP77 payjoin.", )); } if has_onchain_melt { experimental_features.push(( "On-chain Send", - "Withdraw ecash directly to an on-chain Bitcoin address (Ark offboard).", + "Withdraw ecash directly to an on-chain Bitcoin address.", )); } if has_onchain_mint { experimental_features.push(( "Proof of Reserves", - "Reserve attestations via Ark self-spend, independently verifiable against the Ark server's key.", + "Reserve attestations via Bark, independently verifiable against the Ark server's key.", )); } @@ -1041,20 +1041,18 @@ pub(crate) async fn get_index( } } - // Experimental features section (genuinely-added, non-NUT capabilities) + // Experimental features section (genuinely-added, non-NUT capabilities) — full width @if !experimental_features.is_empty() { div class="card-section-header has-rule" { "Experimental features" } div style="padding-top:12px" { - div class="features-grid" { - @for (feat_name, feat_desc) in &experimental_features { - div class="feature" { - div class="feature-dot" style="background:var(--yellow-soft)" { - (maud::PreEscaped(r#""#)) - } - div { - span class="feature-name" { (feat_name) } - div style="font-size:11px;color:var(--text-muted);margin-top:3px;line-height:1.45" { (feat_desc) } - } + @for (feat_name, feat_desc) in &experimental_features { + div class="feature" style="border-right:none" { + div class="feature-dot" style="background:var(--yellow-soft)" { + (maud::PreEscaped(r#""#)) + } + div { + span class="feature-name" { (feat_name) } + div style="font-size:11px;color:var(--text-muted);margin-top:3px;line-height:1.45" { (feat_desc) } } } } From 2f7752240e58f193e59d2c1fe894cac306ee1be3 Mon Sep 17 00:00:00 2001 From: Nordic Operator Date: Sun, 7 Jun 2026 10:09:28 +0200 Subject: [PATCH 3/6] cdk-axum: serve PoR bundle at GET /audit/latest.json (CORS-open, file-backed) Signed-off-by: Matthew Vuk --- crates/cdk-axum/src/lib.rs | 3 +++ crates/cdk-axum/src/router_handlers.rs | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index 7f8409cdbb..ae09f38fe5 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -116,6 +116,9 @@ pub async fn create_mint_router_with_custom_cache( mint_router = mint_router.route("/", get(get_index)); } + // Proof-of-reserves attestation bundle, served from a file written by the payment processor. + mint_router = mint_router.route("/audit/latest.json", get(get_audit_latest)); + let mint_router = { let auth_router = create_auth_router(state.clone()); mint_router.nest("/v1", auth_router) diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index f696b8a3b4..4e4718bfae 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -15,6 +15,30 @@ use tracing::instrument; use crate::auth::AuthHeader; use crate::ws::main_websocket; + +/// Serve the proof-of-reserves attestation bundle (written by the payment processor to a file). +/// Path from env `CDK_AUDIT_BUNDLE_PATH`, default `~/mint/audit/latest.json`. 404 if absent. +/// CORS-open so wallets can fetch it from any origin. +pub(crate) async fn get_audit_latest() -> Response { + let path = std::env::var("CDK_AUDIT_BUNDLE_PATH").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + format!("{home}/mint/audit/latest.json") + }); + match std::fs::read(&path) { + Ok(bytes) => Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, "application/json") + .header(axum::http::header::CACHE_CONTROL, "no-cache") + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(axum::body::Body::from(bytes)) + .unwrap(), + Err(_) => Response::builder() + .status(StatusCode::NOT_FOUND) + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(axum::body::Body::from("no attestation available")) + .unwrap(), + } +} use crate::MintState; /// Macro to add cache to endpoint @@ -866,7 +890,7 @@ pub(crate) async fn get_index( if has_onchain_mint { experimental_features.push(( "Proof of Reserves", - "Reserve attestations via Bark, independently verifiable against the Ark server's key.", + "Reserve attestations via Bark, independently verifiable.", )); } From 1ccbe57a28c5e53b5f81e7fc07d30a7b761ac62e Mon Sep 17 00:00:00 2001 From: Nordic Operator Date: Sun, 7 Jun 2026 11:14:48 +0200 Subject: [PATCH 4/6] cdk-axum info-page: render live PoR attestation (reserve/block + bundle link) Signed-off-by: Matthew Vuk --- crates/cdk-axum/src/router_handlers.rs | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 4e4718bfae..8ce57e7840 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -894,6 +894,25 @@ pub(crate) async fn get_index( )); } + // Live Proof-of-Reserves attestation, read from the bundle the payment processor writes. + // (total_reserve_sat, as_of_block.height, reserve VTXO count) + let por_attestation: Option<(u64, u64, usize)> = { + let path = std::env::var("CDK_AUDIT_BUNDLE_PATH").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + format!("{home}/mint/audit/latest.json") + }); + std::fs::read(path) + .ok() + .and_then(|b| serde_json::from_slice::(&b).ok()) + .map(|v| { + ( + v["total_reserve_sat"].as_u64().unwrap_or(0), + v["as_of_block"]["height"].as_u64().unwrap_or(0), + v["reserve"].as_array().map(|a| a.len()).unwrap_or(0), + ) + }) + }; + // Avatar fallback letter let avatar_letter = name .chars() @@ -1083,6 +1102,26 @@ pub(crate) async fn get_index( } } + // Live Proof-of-Reserves attestation (rendered from the latest bundle) + @if let Some((reserve, block, n)) = por_attestation { + div class="card-section-header has-rule" { "Proof of Reserves" } + div style="padding:14px 20px 18px" { + div { + span style="font-size:28px;font-weight:700;color:var(--green)" { (reserve) } + span style="color:var(--text-muted);font-size:15px" { " sat" } + } + div style="color:var(--text-secondary);font-size:12px;margin-top:3px" { + "lower bound across " (n) " live Ark VTXO(s) — as of block " (block) + } + div style="margin-top:10px" { + a href="/audit/latest.json" style="color:var(--green);font-size:12px;text-decoration:none" { "↓ attestation bundle (JSON)" } + } + div style="color:var(--text-faint);font-size:11px;margin-top:8px;line-height:1.45" { + "Independently verifiable against the Ark server's cosign key. Proof of reserves — a lower bound on assets, not solvency." + } + } + } + // Contact section @if !contact.is_empty() { div class="card-section-header has-rule" { "Contact" } From 498da9f400171ceb642320e30e05c631b6ed90ce Mon Sep 17 00:00:00 2001 From: Nordic Operator Date: Sun, 7 Jun 2026 11:42:58 +0200 Subject: [PATCH 5/6] info-page PoR: 'Lower bound across N live Bark VTXO(s) as of block X' Signed-off-by: Matthew Vuk --- crates/cdk-axum/src/router_handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 8ce57e7840..969e0d9d74 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -1111,7 +1111,7 @@ pub(crate) async fn get_index( span style="color:var(--text-muted);font-size:15px" { " sat" } } div style="color:var(--text-secondary);font-size:12px;margin-top:3px" { - "lower bound across " (n) " live Ark VTXO(s) — as of block " (block) + "Lower bound across " (n) " live Bark VTXO(s) as of block " (block) } div style="margin-top:10px" { a href="/audit/latest.json" style="color:var(--green);font-size:12px;text-decoration:none" { "↓ attestation bundle (JSON)" } From db75e187357eef536e31324c21b42f6a356eca31 Mon Sep 17 00:00:00 2001 From: Nordic Operator Date: Sun, 7 Jun 2026 15:44:21 +0200 Subject: [PATCH 6/6] cdk-axum: serve mint icon at /icon.png (file-backed, CORS) Signed-off-by: Matthew Vuk --- crates/cdk-axum/src/lib.rs | 3 +++ crates/cdk-axum/src/router_handlers.rs | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/crates/cdk-axum/src/lib.rs b/crates/cdk-axum/src/lib.rs index ae09f38fe5..00a26c630a 100644 --- a/crates/cdk-axum/src/lib.rs +++ b/crates/cdk-axum/src/lib.rs @@ -119,6 +119,9 @@ pub async fn create_mint_router_with_custom_cache( // Proof-of-reserves attestation bundle, served from a file written by the payment processor. mint_router = mint_router.route("/audit/latest.json", get(get_audit_latest)); + // Mint icon (PNG), served from a file. + mint_router = mint_router.route("/icon.png", get(get_mint_icon)); + let mint_router = { let auth_router = create_auth_router(state.clone()); mint_router.nest("/v1", auth_router) diff --git a/crates/cdk-axum/src/router_handlers.rs b/crates/cdk-axum/src/router_handlers.rs index 969e0d9d74..33165f109b 100644 --- a/crates/cdk-axum/src/router_handlers.rs +++ b/crates/cdk-axum/src/router_handlers.rs @@ -39,6 +39,28 @@ pub(crate) async fn get_audit_latest() -> Response { .unwrap(), } } + +/// Serve the mint icon (PNG). Path from env `CDK_MINT_ICON_PATH`, default `~/mint/icon.png`. +pub(crate) async fn get_mint_icon() -> Response { + let path = std::env::var("CDK_MINT_ICON_PATH").unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string()); + format!("{home}/mint/icon.png") + }); + match std::fs::read(&path) { + Ok(bytes) => Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, "image/png") + .header(axum::http::header::CACHE_CONTROL, "public, max-age=86400") + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(axum::body::Body::from(bytes)) + .unwrap(), + Err(_) => Response::builder() + .status(StatusCode::NOT_FOUND) + .header(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN, "*") + .body(axum::body::Body::from("no icon")) + .unwrap(), + } +} use crate::MintState; /// Macro to add cache to endpoint