nwc: add downloadable/printable LNURL QR for wallet connections

Each NWC wallet connection gets its own shareable LNURL QR so the owner
can receive payments from anyone without creating invoices (the core
LNURL value proposition: a static QR that never expires).

Hub UI (service detail modal):
- New 'Share QR' button on every wallet connection card
- Dedicated share view: large QR, Lightning Address + copy, raw bech32
  LNURL + copy
- Download PNG (websites/social), download SVG (vector, print-sharp at
  any size), and a Print button that opens a print-ready payment card
  which auto-invokes the browser print dialog

Backend (FastAPI, session-authenticated):
- GET /api/nwc/wallets/{id}/lnurl -> alias, lightning_address, LNURL
- GET /api/nwc/wallets/{id}/lnurl-qr.png (?download=1, ?scale=)
- GET /api/nwc/wallets/{id}/lnurl-qr.svg (?download=1)
- GET /api/nwc/wallets/{id}/lnurl-qr/print (print-ready HTML card)
- Stdlib bech32 encoder (BIP-173, verified against the official LUD-01
  golden vector); LNURL is uppercased for QR alphanumeric-mode density
  per LUD-01 guidance; QR rendered with the existing qrencode package
- Resolves wallets by app id or pubkey; 404 unknown wallet, 503 when no
  Lightning domain is configured; all routes return 401 unauthenticated

No new system dependencies.

Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
This commit is contained in:
naturallaw777
2026-07-28 23:47:37 +00:00
co-authored by arena-agent
parent 5ab2e432f1
commit 5e3432aa89
3 changed files with 346 additions and 9 deletions
+239 -6
View File
@@ -26,10 +26,11 @@ import urllib.request
from threading import Lock from threading import Lock
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.requests import Request from fastapi.requests import Request
from html import escape as _html_escape
from pydantic import BaseModel from pydantic import BaseModel
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
@@ -1341,22 +1342,101 @@ def _evaluate_domain_checklist(
# ── QR code helper ──────────────────────────────────────────────── # ── QR code helper ────────────────────────────────────────────────
def _generate_qr_base64(data: str) -> str | None: def _generate_qr_png_bytes(data: str, scale: int = 6, margin: int = 2) -> bytes | None:
"""Generate a QR code PNG and return it as a base64-encoded data URI. """Generate a QR code PNG and return the raw bytes.
Uses qrencode CLI (available on the system via credentials.nix).""" Uses qrencode CLI (available on the system via credentials.nix)."""
try: try:
result = subprocess.run( result = subprocess.run(
["qrencode", "-o", "-", "-t", "PNG", "-s", "6", "-m", "2", "-l", "H", data], ["qrencode", "-o", "-", "-t", "PNG", "-s", str(scale), "-m", str(margin), "-l", "H", data],
capture_output=True, timeout=10, capture_output=True, timeout=10,
) )
if result.returncode == 0 and result.stdout: if result.returncode == 0 and result.stdout:
b64 = base64.b64encode(result.stdout).decode("ascii") return result.stdout
return f"data:image/png;base64,{b64}"
except Exception: except Exception:
pass pass
return None return None
def _generate_qr_svg(data: str, scale: int = 10, margin: int = 4) -> str | None:
"""Generate a QR code SVG document (resolution-independent, ideal if the
user wants to embed the QR in a website or print it at any size)."""
try:
result = subprocess.run(
["qrencode", "-o", "-", "-t", "SVG", "-s", str(scale), "-m", str(margin), "-l", "H", data],
capture_output=True, timeout=10,
)
if result.returncode == 0 and result.stdout:
return result.stdout.decode("utf-8", errors="replace")
except Exception:
pass
return None
def _generate_qr_base64(data: str) -> str | None:
"""Generate a QR code PNG and return it as a base64-encoded data URI."""
png = _generate_qr_png_bytes(data, scale=6, margin=2)
if png:
b64 = base64.b64encode(png).decode("ascii")
return f"data:image/png;base64,{b64}"
return None
# ── Bech32 encoding (BIP-173) — used for LNURL strings (LUD-01) ──
_BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
_BECH32_GENERATOR = (0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3)
def _bech32_polymod(values: list[int]) -> int:
chk = 1
for value in values:
top = chk >> 25
chk = ((chk & 0x1FFFFFF) << 5) ^ value
for i in range(5):
if (top >> i) & 1:
chk ^= _BECH32_GENERATOR[i]
return chk
def _bech32_hrp_expand(hrp: str) -> list[int]:
return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
def _bech32_create_checksum(hrp: str, data: list[int]) -> list[int]:
values = _bech32_hrp_expand(hrp) + data
polymod = _bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1
return [(polymod >> (5 * (5 - i))) & 31 for i in range(6)]
def _bech32_convertbits(data: bytes, frombits: int, tobits: int) -> list[int]:
acc = 0
bits = 0
ret: list[int] = []
maxv = (1 << tobits) - 1
for value in data:
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if bits:
ret.append((acc << (tobits - bits)) & maxv)
return ret
def _bech32_encode(hrp: str, payload: bytes) -> str:
"""Encode payload bytes as a bech32 string with the given HRP (BIP-173)."""
data = _bech32_convertbits(payload, 8, 5)
combined = data + _bech32_create_checksum(hrp, data)
return hrp + "1" + "".join(_BECH32_CHARSET[d] for d in combined)
def _nwc_lnurl_bech32(alias: str, domain: str) -> str:
"""Return the LUD-01 bech32 LNURL for a wallet connection alias."""
url = f"https://{domain}/.well-known/lnurlp/{alias}"
return _bech32_encode("lnurl", url.encode("utf-8"))
# ── Update helpers (file-based, no systemctl) ──────────────────── # ── Update helpers (file-based, no systemctl) ────────────────────
def _read_update_status() -> str: def _read_update_status() -> str:
@@ -4550,6 +4630,159 @@ async def api_nwc_test(alias: str):
return {"ok": True} return {"ok": True}
# ── LNURL QR sharing (download / print) ───────────────────────────
def _nwc_lnurl_context(wallet_identifier: str) -> tuple[dict | None, str | None, JSONResponse | None]:
"""Resolve a wallet connection + domain for the LNURL QR endpoints.
Returns ``(wallet, domain, error_response)``. On success the error is
``None``; on failure the wallet is ``None`` and the error is a ready-made
``JSONResponse``. Blocking — call via ``loop.run_in_executor``.
"""
domain = _nwc_domain()
if not domain:
return None, None, _nwc_error(503, "domain_not_configured", "Lightning Address domain is not configured.")
needle = wallet_identifier.strip().lower()
try:
wallets = _nwc_mgr.get_manager().list_wallets(domain)
except _nwc_mgr.AlbyHubError as exc:
return None, domain, _nwc_error(503, exc.code, exc.args[0])
for wallet in wallets:
wid = str(wallet.get("id", "")).lower()
wpk = str(wallet.get("pubkey", "")).lower()
if needle and needle in (wid, wpk):
if not wallet.get("alias"):
return None, domain, _nwc_error(404, "wallet_not_found", "Wallet connection has no Lightning Address alias.")
return wallet, domain, None
return None, domain, _nwc_error(404, "wallet_not_found", "Wallet connection not found.")
def _nwc_lnurl_error_png() -> JSONResponse:
return _nwc_error(500, "qr_unavailable", "QR code generation failed on this system.")
@app.get("/api/nwc/wallets/{wallet_identifier}/lnurl")
async def api_nwc_wallet_lnurl(wallet_identifier: str):
"""Return the bech32 LNURL + Lightning Address for a wallet connection."""
loop = asyncio.get_event_loop()
wallet, domain, err = await loop.run_in_executor(None, _nwc_lnurl_context, wallet_identifier)
if err:
return err
return {
"alias": wallet["alias"],
"lightning_address": wallet.get("lightning_address"),
"lnurl": _nwc_lnurl_bech32(wallet["alias"], domain),
}
@app.get("/api/nwc/wallets/{wallet_identifier}/lnurl-qr.png")
async def api_nwc_wallet_lnurl_qr_png(wallet_identifier: str, download: bool = False, scale: int = 10):
"""Serve the LNURL QR code as a PNG (uppercase LNURL for scannability).
``?download=1`` switches Content-Disposition to attachment so the browser
saves it with a meaningful filename. ``scale`` (qrencode pixel size) may be
raised for high-resolution display, e.g. the print page.
"""
loop = asyncio.get_event_loop()
wallet, domain, err = await loop.run_in_executor(None, _nwc_lnurl_context, wallet_identifier)
if err:
return err
scale = max(1, min(scale, 40))
lnurl_qr = _nwc_lnurl_bech32(wallet["alias"], domain).upper()
png = await loop.run_in_executor(None, lambda: _generate_qr_png_bytes(lnurl_qr, scale=scale, margin=4))
if png is None:
return _nwc_lnurl_error_png()
filename = f"lightning-{wallet['alias']}-{domain}.png"
disposition = "attachment" if download else "inline"
return Response(
content=png,
media_type="image/png",
headers={
"Content-Disposition": f'{disposition}; filename="{filename}"',
"Cache-Control": "no-store",
},
)
@app.get("/api/nwc/wallets/{wallet_identifier}/lnurl-qr.svg")
async def api_nwc_wallet_lnurl_qr_svg(wallet_identifier: str, download: bool = False):
"""Serve the LNURL QR code as an SVG — resolution-independent, so it stays
perfectly sharp when printed at any size or embedded on a website."""
loop = asyncio.get_event_loop()
wallet, domain, err = await loop.run_in_executor(None, _nwc_lnurl_context, wallet_identifier)
if err:
return err
lnurl_qr = _nwc_lnurl_bech32(wallet["alias"], domain).upper()
svg = await loop.run_in_executor(None, lambda: _generate_qr_svg(lnurl_qr, margin=4))
if svg is None:
return _nwc_error(500, "qr_unavailable", "QR code generation failed on this system.")
filename = f"lightning-{wallet['alias']}-{domain}.svg"
disposition = "attachment" if download else "inline"
return Response(
content=svg.encode("utf-8"),
media_type="image/svg+xml",
headers={
"Content-Disposition": f'{disposition}; filename="{filename}"',
"Cache-Control": "no-store",
},
)
@app.get("/api/nwc/wallets/{wallet_identifier}/lnurl-qr/print")
async def api_nwc_wallet_lnurl_qr_print(wallet_identifier: str):
"""Print-ready LNURL payment card — opens the browser print dialog."""
loop = asyncio.get_event_loop()
wallet, domain, err = await loop.run_in_executor(None, _nwc_lnurl_context, wallet_identifier)
if err:
return err
address = wallet.get("lightning_address") or f"{wallet['alias']}@{domain}"
name = _html_escape(str(wallet.get("name") or "Wallet"))
addr_html = _html_escape(address)
img_src = f"/api/nwc/wallets/{urllib.parse.quote(wallet_identifier, safe='')}/lnurl-qr.png?scale=24"
html_doc = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{addr_html}</title>
<style>
body {{ font-family: system-ui, sans-serif; display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:95vh; margin:0; color:#111; }}
.card {{ text-align:center; padding:28px 36px; border:2px solid #111; border-radius:18px; max-width:90vw; }}
h1 {{ font-size:20px; margin:0 0 18px; font-weight:700; }}
img {{ width:64mm; max-width:70vw; image-rendering:pixelated; }}
.addr {{ font-family:ui-monospace, monospace; font-size:15px; margin-top:18px; word-break:break-all; }}
.hint {{ font-size:12px; color:#555; margin-top:10px; }}
.actions {{ margin-top:22px; display:flex; gap:12px; }}
.actions button {{ font:inherit; padding:10px 22px; border-radius:10px; border:1px solid #111; background:#111; color:#fff; cursor:pointer; }}
.actions button.secondary {{ background:#fff; color:#111; }}
@media print {{ .actions {{ display:none; }} body {{ min-height:auto; }} }}
</style>
</head>
<body>
<div class="card">
<h1>Pay {name} with Bitcoin ⚡</h1>
<img src="{img_src}" alt="LNURL QR code for {addr_html}">
<div class="addr">{addr_html}</div>
<div class="hint">Scan with any Lightning wallet — this QR never expires.</div>
</div>
<div class="actions">
<button onclick="window.print()">🖨 Print</button>
<button class="secondary" onclick="window.close()">Close</button>
</div>
<script>
window.addEventListener("load", function () {{
var img = document.querySelector("img");
function go() {{ setTimeout(function () {{ window.print(); }}, 250); }}
if (img && !img.complete) {{ img.addEventListener("load", go); img.addEventListener("error", go); }}
else {{ go(); }}
}});
</script>
</body>
</html>"""
return HTMLResponse(content=html_doc, headers={"Cache-Control": "no-store"})
# ── Security endpoints ──────────────────────────────────────────── # ── Security endpoints ────────────────────────────────────────────
+26 -1
View File
@@ -517,7 +517,32 @@ button.btn-reboot:hover:not(:disabled) {
} }
.nwc-wallet-actions .matrix-form-back, .nwc-wallet-actions .matrix-form-back,
.nwc-wallet-actions .btn-close-modal { .nwc-wallet-actions .btn-close-modal,
.nwc-wallet-actions .matrix-action-btn {
padding: 8px 12px; padding: 8px 12px;
font-size: 0.82rem; font-size: 0.82rem;
} }
/* ── NWC LNURL share / download view ───────────────────────────── */
.nwc-share-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 14px;
}
.nwc-share-actions .matrix-form-back,
.nwc-share-actions .matrix-form-submit {
padding: 8px 12px;
font-size: 0.82rem;
}
.nwc-lnurl-value {
font-size: 0.72rem;
word-break: break-all;
}
.nwc-share-formats-hint {
text-align: center;
}
@@ -178,6 +178,64 @@ function _nwcRenderWalletState() {
return; return;
} }
if (state.view === "share" && state.shareWallet) {
var sw = state.shareWallet;
var swId = encodeURIComponent(String(sw.id || sw.pubkey || ""));
var swAddress = sw.lightning_address || ((sw.alias && state.domain) ? sw.alias + "@" + state.domain : "");
var pngUrl = "/api/nwc/wallets/" + swId + "/lnurl-qr.png";
var shareAddrId = "nwc-share-addr-" + Math.random().toString(36).substring(2, 8);
var shareLnurlId = "nwc-share-lnurl-" + Math.random().toString(36).substring(2, 8);
html += '<p class="svc-detail-desc">Share the Lightning Address for <strong>' + escHtml(sw.name || "Wallet") + '</strong>. This QR never expires — anyone can scan it with any Lightning wallet to send sats, as many times as they like.</p>' +
'<div class="creds-qr-wrap">' +
'<img class="creds-qr-img" src="' + pngUrl + '" alt="LNURL QR code for ' + escHtml(swAddress) + '">' +
'<div class="creds-qr-hint">Scan with any Lightning wallet to pay ' + escHtml(swAddress) + '</div>' +
'</div>' +
(swAddress
? '<div class="creds-row"><div class="creds-label">Lightning Address</div><div class="creds-value-wrap"><div class="creds-value" id="' + shareAddrId + '">' + escHtml(swAddress) + '</div><button class="creds-copy-btn" data-target="' + shareAddrId + '">Copy</button></div></div>'
: "") +
'<div class="creds-row"><div class="creds-label">LNURL</div><div class="creds-value-wrap"><div class="creds-value nwc-lnurl-value" id="' + shareLnurlId + '">Loading…</div><button class="creds-copy-btn" data-target="' + shareLnurlId + '">Copy</button></div></div>' +
'<div class="nwc-share-actions">' +
'<button class="matrix-form-submit" id="nwc-share-dl-png-btn">⬇ Download PNG</button>' +
'<button class="matrix-form-back" id="nwc-share-dl-svg-btn">⬇ Download SVG</button>' +
'<button class="matrix-form-back" id="nwc-share-print-btn">🖨 Print…</button>' +
'<button class="matrix-form-back" id="nwc-share-back-btn">← Back</button>' +
'</div>' +
'<div class="creds-qr-hint nwc-share-formats-hint">PNG is ideal for websites and social posts. SVG stays sharp at any print size.</div>';
host.innerHTML = html;
_attachCopyHandlers(host);
apiFetch("/api/nwc/wallets/" + swId + "/lnurl").then(function(payload) {
var el = document.getElementById(shareLnurlId);
if (el) el.textContent = payload.lnurl || "Unavailable";
}).catch(function() {
var el = document.getElementById(shareLnurlId);
if (el) el.textContent = "Unavailable";
});
function _nwcShareDownload(url) {
var a = document.createElement("a");
a.href = url;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
}
var dlPngBtn = document.getElementById("nwc-share-dl-png-btn");
if (dlPngBtn) dlPngBtn.addEventListener("click", function() { _nwcShareDownload(pngUrl + "?download=1"); });
var dlSvgBtn = document.getElementById("nwc-share-dl-svg-btn");
if (dlSvgBtn) dlSvgBtn.addEventListener("click", function() { _nwcShareDownload("/api/nwc/wallets/" + swId + "/lnurl-qr.svg?download=1"); });
var printBtn = document.getElementById("nwc-share-print-btn");
if (printBtn) printBtn.addEventListener("click", function() { window.open("/api/nwc/wallets/" + swId + "/lnurl-qr/print", "_blank", "noopener"); });
var shareBackBtn = document.getElementById("nwc-share-back-btn");
if (shareBackBtn) {
shareBackBtn.addEventListener("click", function() {
state.view = "list";
state.shareWallet = null;
_nwcClearMessage();
_nwcRenderWalletState();
});
}
return;
}
var canCreate = !!state.domain && !state.busy; var canCreate = !!state.domain && !state.busy;
html += '<div class="matrix-actions-row">' + html += '<div class="matrix-actions-row">' +
'<button class="matrix-action-btn" id="nwc-open-create-btn"' + (canCreate ? "" : " disabled") + '>' + (state.domain ? ' Create Wallet Connection' : 'Configure Domain First') + '</button>' + '<button class="matrix-action-btn" id="nwc-open-create-btn"' + (canCreate ? "" : " disabled") + '>' + (state.domain ? ' Create Wallet Connection' : 'Configure Domain First') + '</button>' +
@@ -219,6 +277,7 @@ function _nwcRenderWalletState() {
'<div class="creds-row"><div class="creds-label">Balance</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(String(wallet.balance_sats || 0)) + ' sats</div></div></div>' + '<div class="creds-row"><div class="creds-label">Balance</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(String(wallet.balance_sats || 0)) + ' sats</div></div></div>' +
'<div class="creds-row"><div class="creds-label">Pending TX</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(String(wallet.pending_transactions || 0)) + '</div></div></div>' + '<div class="creds-row"><div class="creds-label">Pending TX</div><div class="creds-value-wrap"><div class="creds-value">' + escHtml(String(wallet.pending_transactions || 0)) + '</div></div></div>' +
'<div class="nwc-wallet-actions">' + '<div class="nwc-wallet-actions">' +
'<button class="matrix-action-btn nwc-wallet-action-btn" data-action="share" data-wallet-id="' + escHtml(id) + '"' + ((state.busy || !wallet.lightning_address) ? " disabled" : "") + '>⚡ Share QR</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn" data-action="test" data-wallet-alias="' + escHtml(wallet.alias || "") + '"' + (state.busy ? " disabled" : "") + '>Verify Address</button>' + '<button class="matrix-form-back nwc-wallet-action-btn" data-action="test" data-wallet-alias="' + escHtml(wallet.alias || "") + '"' + (state.busy ? " disabled" : "") + '>Verify Address</button>' +
'<button class="matrix-form-back nwc-wallet-action-btn" data-action="drain" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Drain</button>' + '<button class="matrix-form-back nwc-wallet-action-btn" data-action="drain" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Drain</button>' +
'<button class="btn btn-close-modal nwc-wallet-action-btn" data-action="delete" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Delete</button>' + '<button class="btn btn-close-modal nwc-wallet-action-btn" data-action="delete" data-wallet-id="' + escHtml(id) + '"' + (state.busy ? " disabled" : "") + '>Delete</button>' +
@@ -245,13 +304,32 @@ function _nwcRenderWalletState() {
host.querySelectorAll(".nwc-wallet-action-btn").forEach(function(btn) { host.querySelectorAll(".nwc-wallet-action-btn").forEach(function(btn) {
btn.addEventListener("click", function() { btn.addEventListener("click", function() {
var action = btn.getAttribute("data-action"); var action = btn.getAttribute("data-action");
if (action === "test") _nwcVerifyWallet(btn.getAttribute("data-wallet-alias")); if (action === "share") _nwcOpenShare(btn.getAttribute("data-wallet-id"));
else if (action === "test") _nwcVerifyWallet(btn.getAttribute("data-wallet-alias"));
else if (action === "drain") _nwcDrainWallet(btn.getAttribute("data-wallet-id")); else if (action === "drain") _nwcDrainWallet(btn.getAttribute("data-wallet-id"));
else if (action === "delete") _nwcDeleteWallet(btn.getAttribute("data-wallet-id")); else if (action === "delete") _nwcDeleteWallet(btn.getAttribute("data-wallet-id"));
}); });
}); });
} }
function _nwcOpenShare(walletId) {
if (!_nwcModalState || _nwcModalState.busy || !walletId) return;
var wallet = null;
for (var i = 0; i < _nwcModalState.wallets.length; i++) {
var w = _nwcModalState.wallets[i];
if (String(w.id || w.pubkey || "") === String(walletId)) { wallet = w; break; }
}
if (!wallet) {
_nwcSetMessage("error", "Wallet connection not found.");
_nwcRenderWalletState();
return;
}
_nwcClearMessage();
_nwcModalState.shareWallet = wallet;
_nwcModalState.view = "share";
_nwcRenderWalletState();
}
async function _nwcRefreshWallets() { async function _nwcRefreshWallets() {
if (!_nwcModalState) return; if (!_nwcModalState) return;
var host = _nwcWalletSectionEl(); var host = _nwcWalletSectionEl();
@@ -260,7 +338,7 @@ async function _nwcRefreshWallets() {
var payload = await apiFetch("/api/nwc/wallets"); var payload = await apiFetch("/api/nwc/wallets");
_nwcModalState.wallets = Array.isArray(payload.wallets) ? payload.wallets : []; _nwcModalState.wallets = Array.isArray(payload.wallets) ? payload.wallets : [];
_nwcModalState.domain = payload.domain || null; _nwcModalState.domain = payload.domain || null;
if (_nwcModalState.view !== "create" && _nwcModalState.view !== "created") { if (_nwcModalState.view !== "create" && _nwcModalState.view !== "created" && _nwcModalState.view !== "share") {
_nwcModalState.view = _nwcModalState.wallets.length > 0 ? "list" : "empty"; _nwcModalState.view = _nwcModalState.wallets.length > 0 ? "list" : "empty";
} }
_nwcRenderWalletState(); _nwcRenderWalletState();
@@ -409,6 +487,7 @@ async function _nwcInitWalletFlow(unit, name, icon) {
message: "", message: "",
messageKind: "", messageKind: "",
lastCreated: null, lastCreated: null,
shareWallet: null,
createForm: { createForm: {
name: "", name: "",
alias: "", alias: "",