diff --git a/app/sovran_systemsos_web/server.py b/app/sovran_systemsos_web/server.py index 5b25c83..b2835db 100644 --- a/app/sovran_systemsos_web/server.py +++ b/app/sovran_systemsos_web/server.py @@ -4422,9 +4422,11 @@ async def api_nwc_create_wallet(req: NwcWalletCreateRequest): domain = _nwc_domain() verify = _nwc_test_address(alias) + pairing_uri = _nwc_pairing_uri(wallet_id, pairing_secret) + pairing_qrcode = _generate_qr_base64(pairing_uri) response = { "wallet": _nwc_wallet_meta(wallet, domain), - "pairing_uri": _nwc_pairing_uri(wallet_id, pairing_secret), + "pairing_uri": pairing_uri, "lightning_address": f"{alias}@{domain}" if domain else None, "result": { "wallet_created": True, @@ -4433,6 +4435,8 @@ async def api_nwc_create_wallet(req: NwcWalletCreateRequest): "public_endpoint_verification": verify, }, } + if pairing_qrcode: + response["pairing_qrcode"] = pairing_qrcode return JSONResponse(status_code=201, content=response) diff --git a/app/sovran_systemsos_web/static/css/modals.css b/app/sovran_systemsos_web/static/css/modals.css index c08322a..0894990 100644 --- a/app/sovran_systemsos_web/static/css/modals.css +++ b/app/sovran_systemsos_web/static/css/modals.css @@ -478,3 +478,46 @@ button.btn-reboot:hover:not(:disabled) { color: var(--text-secondary); font-style: italic; } + +.nwc-secret-warning { + margin: 0 0 14px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid #f59e0b; + background: rgba(245, 158, 11, 0.12); + color: #fbbf24; + font-size: 0.86rem; +} + +.nwc-wallet-list { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; +} + +.nwc-wallet-card { + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 12px; + background: rgba(255, 255, 255, 0.02); +} + +.nwc-wallet-card-title { + font-weight: 700; + margin-bottom: 8px; + color: var(--text-primary); +} + +.nwc-wallet-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 10px; +} + +.nwc-wallet-actions .matrix-form-back, +.nwc-wallet-actions .btn-close-modal { + padding: 8px 12px; + font-size: 0.82rem; +} diff --git a/app/sovran_systemsos_web/static/js/service-detail.js b/app/sovran_systemsos_web/static/js/service-detail.js index 036e90f..5022a6a 100644 --- a/app/sovran_systemsos_web/static/js/service-detail.js +++ b/app/sovran_systemsos_web/static/js/service-detail.js @@ -58,6 +58,359 @@ function _attachCopyHandlers(container) { }); } +var _nwcModalState = null; + +function _nwcStateMessageHtml() { + if (!_nwcModalState || !_nwcModalState.message) return ""; + var msgClass = _nwcModalState.messageKind === "success" ? "success" : "error"; + return '
Create a new isolated wallet connection for your app.
' + + 'Lightning Address domain: ' + escHtml(state.domain) + '
'; + } else { + html += 'Lightning Address domain is not configured yet. Configure your domain first, then create wallet connections.
'; + } + + if (!state.wallets || state.wallets.length === 0) { + html += 'No wallet connections yet. Create your first wallet connection to generate a one-time pairing secret.
'; + host.innerHTML = html; + var openCreateBtn = document.getElementById("nwc-open-create-btn"); + if (openCreateBtn) { + openCreateBtn.addEventListener("click", function() { + if (state.busy) return; + _nwcClearMessage(); + state.view = "create"; + _nwcRenderWalletState(); + }); + } + var refreshBtnEmpty = document.getElementById("nwc-refresh-btn"); + if (refreshBtnEmpty) refreshBtnEmpty.addEventListener("click", _nwcRefreshWallets); + return; + } + + html += 'Loading wallet connections…
'; + try { + var payload = await apiFetch("/api/nwc/wallets"); + _nwcModalState.wallets = Array.isArray(payload.wallets) ? payload.wallets : []; + _nwcModalState.domain = payload.domain || null; + if (_nwcModalState.view !== "create" && _nwcModalState.view !== "created") { + _nwcModalState.view = _nwcModalState.wallets.length > 0 ? "list" : "empty"; + } + _nwcRenderWalletState(); + } catch (err) { + _nwcSetMessage("error", (err && err.message) ? err.message : "Could not load wallet connections."); + _nwcRenderWalletState(); + } +} + +function _nwcBusy(on) { + if (!_nwcModalState) return; + _nwcModalState.busy = !!on; +} + +async function _nwcCreateWallet() { + if (!_nwcModalState || _nwcModalState.busy) return; + var nameEl = document.getElementById("nwc-wallet-name"); + var aliasEl = document.getElementById("nwc-wallet-alias"); + var presetEl = document.getElementById("nwc-wallet-preset"); + var limitEl = document.getElementById("nwc-wallet-limit"); + if (!nameEl || !aliasEl || !presetEl || !limitEl) return; + + var name = (nameEl.value || "").trim(); + var alias = (aliasEl.value || "").trim().toLowerCase(); + var preset = presetEl.value || "receive_only"; + var limitRaw = (limitEl.value || "").trim(); + var limit = null; + + _nwcModalState.createForm = { + name: name, + alias: alias, + access_preset: preset, + spending_limit_sats: limitRaw + }; + + if (!name) { + _nwcSetMessage("error", "Wallet Connection name is required."); + _nwcRenderWalletState(); + return; + } + if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(alias)) { + _nwcSetMessage("error", 'Alias must start with a letter or number and use only lowercase letters, numbers, "_" or "-".'); + _nwcRenderWalletState(); + return; + } + if (preset === "send_receive_limited") { + limit = parseInt(limitRaw, 10); + if (!Number.isFinite(limit) || limit <= 0) { + _nwcSetMessage("error", "A positive spending limit is required for limited send access."); + _nwcRenderWalletState(); + return; + } + } + + _nwcBusy(true); + _nwcClearMessage(); + _nwcRenderWalletState(); + try { + var payload = await apiFetch("/api/nwc/wallets", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: name, + alias: alias, + access_preset: preset, + spending_limit_sats: preset === "send_receive_limited" ? limit : null + }) + }); + _nwcModalState.lastCreated = { + wallet: payload.wallet || null, + pairing_uri: payload.pairing_uri || "", + pairing_qrcode: payload.pairing_qrcode || "", + lightning_address: payload.lightning_address || null + }; + _nwcModalState.view = "created"; + _nwcSetMessage("success", "Wallet created. Save the one-time secret before you continue."); + _nwcBusy(false); + _nwcRenderWalletState(); + } catch (err) { + _nwcBusy(false); + _nwcSetMessage("error", (err && err.message) ? err.message : "Failed to create wallet connection."); + _nwcRenderWalletState(); + } +} + +async function _nwcVerifyWallet(alias) { + if (!_nwcModalState || _nwcModalState.busy || !alias) return; + _nwcBusy(true); + _nwcClearMessage(); + _nwcRenderWalletState(); + try { + await apiFetch("/api/nwc/addresses/" + encodeURIComponent(alias) + "/test", { method: "POST" }); + _nwcSetMessage("success", "Lightning Address verification succeeded for " + alias + "."); + } catch (err) { + _nwcSetMessage("error", (err && err.message) ? err.message : "Lightning Address verification failed."); + } + _nwcBusy(false); + _nwcRenderWalletState(); +} + +async function _nwcDrainWallet(walletId) { + if (!_nwcModalState || _nwcModalState.busy || !walletId) return; + if (!window.confirm("Drain this wallet connection now? This cannot be undone.")) return; + _nwcBusy(true); + _nwcClearMessage(); + _nwcRenderWalletState(); + try { + var resp = await apiFetch("/api/nwc/wallets/" + encodeURIComponent(walletId) + "/drain", { method: "POST" }); + _nwcSetMessage("success", "Wallet drained (" + String(resp.drained_sats || 0) + " sats)."); + _nwcBusy(false); + await _nwcRefreshWallets(); + } catch (err) { + _nwcBusy(false); + _nwcSetMessage("error", (err && err.message) ? err.message : "Failed to drain wallet."); + _nwcRenderWalletState(); + } +} + +async function _nwcDeleteWallet(walletId) { + if (!_nwcModalState || _nwcModalState.busy || !walletId) return; + if (!window.confirm("Delete this wallet connection? This removes the wallet alias from Wallet Connections.")) return; + _nwcBusy(true); + _nwcClearMessage(); + _nwcRenderWalletState(); + try { + await apiFetch("/api/nwc/wallets/" + encodeURIComponent(walletId), { method: "DELETE" }); + _nwcSetMessage("success", "Wallet connection deleted."); + _nwcBusy(false); + await _nwcRefreshWallets(); + } catch (err) { + _nwcBusy(false); + _nwcSetMessage("error", (err && err.message) ? err.message : "Failed to delete wallet connection."); + _nwcRenderWalletState(); + } +} + +async function _nwcInitWalletFlow(unit, name, icon) { + _nwcModalState = { + unit: unit, + name: name, + icon: icon, + view: "empty", + wallets: [], + domain: null, + busy: false, + message: "", + messageKind: "", + lastCreated: null, + createForm: { + name: "", + alias: "", + access_preset: "receive_only", + spending_limit_sats: "" + } + }; + await _nwcRefreshWallets(); +} + async function openServiceDetailModal(unit, name, icon) { if (!$credsModal) return; if ($credsTitle) { @@ -242,7 +595,12 @@ async function openServiceDetailModal(unit, name, icon) { } // Section E: Credentials & Links - if (data.has_credentials && data.credentials && data.credentials.length > 0) { + if (unit === "nwc-wallets.service") { + html += 'Loading wallet connections…