- Add rate limiting to public LNURL endpoints (30 req/min per IP) - Add audit logging for wallet lifecycle events (create, drain, delete, rotate) - Add Unix socket support for Python ↔ Alby Hub communication - Add LND macaroon permission documentation/warning - Add pairing secret rotation API endpoint + CLI command - Make Nostr relay configurable; auto-use Haven relay when enabled - Strengthen domain validation (FQDN only, reject localhost/IP) - Add structured audit log at /var/log/sovran-nwc-audit.log Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com>
132 lines
4.1 KiB
Python
132 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
from . import nwc_hub_manager as _mgr_mod
|
|
from .server import _nwc_domain, _nwc_validate_alias, _nwc_test_address
|
|
|
|
|
|
def _print(data) -> None:
|
|
print(json.dumps(data, indent=2, sort_keys=True))
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(prog="nwc-wallet")
|
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
|
|
create = sub.add_parser("create")
|
|
create.add_argument("name")
|
|
create.add_argument("alias")
|
|
preset_group = create.add_mutually_exclusive_group()
|
|
preset_group.add_argument("--receive-only", action="store_true")
|
|
preset_group.add_argument("--limit-sats", type=int)
|
|
|
|
sub.add_parser("list")
|
|
|
|
drain = sub.add_parser("drain")
|
|
drain.add_argument("wallet")
|
|
|
|
delete = sub.add_parser("delete")
|
|
delete.add_argument("wallet")
|
|
|
|
addr = sub.add_parser("address")
|
|
addr_sub = addr.add_subparsers(dest="address_cmd", required=True)
|
|
addr_show = addr_sub.add_parser("show")
|
|
addr_show.add_argument("alias")
|
|
|
|
rotate = sub.add_parser("rotate")
|
|
rotate.add_argument("wallet")
|
|
|
|
sub.add_parser("health")
|
|
|
|
args = parser.parse_args(argv)
|
|
manager = _mgr_mod.get_manager()
|
|
domain = _nwc_domain()
|
|
|
|
if args.cmd == "list":
|
|
try:
|
|
wallets = manager.list_wallets(domain)
|
|
except _mgr_mod.AlbyHubError as exc:
|
|
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
|
|
return 1
|
|
_print({"wallets": wallets})
|
|
return 0
|
|
|
|
if args.cmd == "health":
|
|
result = manager.health()
|
|
_print(result)
|
|
return 0 if result.get("ok") else 1
|
|
|
|
if args.cmd == "address" and args.address_cmd == "show":
|
|
alias = args.alias.strip().lower()
|
|
test = _nwc_test_address(alias)
|
|
_print(test)
|
|
return 0 if test.get("ok") else 1
|
|
|
|
if args.cmd == "drain":
|
|
try:
|
|
result = manager.drain_wallet(args.wallet)
|
|
except _mgr_mod.AlbyHubError as exc:
|
|
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
|
|
return 1
|
|
_print(result)
|
|
return 0
|
|
|
|
if args.cmd == "delete":
|
|
try:
|
|
result = manager.delete_wallet(args.wallet)
|
|
except _mgr_mod.AlbyHubError as exc:
|
|
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
|
|
return 1
|
|
_print(result)
|
|
return 0
|
|
|
|
if args.cmd == "rotate":
|
|
try:
|
|
result = manager.rotate_wallet_secret(args.wallet)
|
|
except _mgr_mod.AlbyHubError as exc:
|
|
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
|
|
return 1
|
|
_print({
|
|
"wallet_id": result.get("wallet_id", ""),
|
|
"pairing_uri": result.get("pairing_uri", ""),
|
|
"message": result.get("message", "New NWC connection secret generated. Save it now — it will not be shown again."),
|
|
})
|
|
return 0
|
|
|
|
if args.cmd == "create":
|
|
alias = args.alias.strip().lower()
|
|
if not _nwc_validate_alias(alias):
|
|
print("Error: alias_invalid - Alias must be lowercase letters, digits, '_' or '-'.", file=sys.stderr)
|
|
return 1
|
|
access_preset = "send_receive_limited" if args.limit_sats is not None else "receive_only"
|
|
try:
|
|
result = manager.create_wallet(
|
|
args.name.strip(),
|
|
alias,
|
|
access_preset,
|
|
args.limit_sats if access_preset == "send_receive_limited" else None,
|
|
domain,
|
|
)
|
|
except _mgr_mod.AlbyHubError as exc:
|
|
print(f"Error: {exc.code} - {exc}", file=sys.stderr)
|
|
return 1
|
|
# Print the pairing URI once — this is the only time it is shown
|
|
_print(
|
|
{
|
|
"wallet": result["wallet"],
|
|
"pairing_uri": result.get("pairing_uri", ""),
|
|
"message": "Keep the NWC connection secret private. It cannot be displayed again.",
|
|
"result": result.get("result", {}),
|
|
}
|
|
)
|
|
return 0
|
|
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|