commit 9c1148b354b685257051208329ec01cfd4fa5230 Author: zlei9 Date: Sun Mar 29 10:16:49 2026 +0800 Initial commit with translated description diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a30e91 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# LNbits Wallet Manager Skill for OpenClaw + +Enable your OpenClaw assistant to safely and effectively manage an LNbits Lightning Network wallet. + +## Features + +- **Check Balance**: Get your current wallet balance in Satoshis. +- **Create Invoice**: Generate Bolt11 invoices to receive funds, with automatic QR code generation. +- **Pay Invoice**: Safely pay Bolt11 invoices after confirmation and balance checks. +- **Decode Invoice**: Inspect Bolt11 invoices to verify amount and memo. +- **Generate QR Code**: Create a QR code image from any Bolt11 invoice string. +- **Create Wallet**: Easily set up a new LNbits wallet on the demo server. + +## ๐Ÿ›‘ Critical Protocols for Safe Usage ๐Ÿ›‘ + +To ensure secure and responsible handling of your LNbits wallet, this skill enforces strict protocols: + +1. **NEVER Expose Secrets**: The assistant is programmed to **NEVER** display Admin Keys, User IDs, or Wallet IDs in chat. Credentials are handled via environment variables. +2. **Explicit Payment Confirmation**: The assistant **MUST** ask for "Yes/No" confirmation before sending any payment. + * **Confirmation Format**: "I am about to send **[Amount] sats** to **[Memo/Destination]**. Proceed? (y/n)" +3. **Balance Check Before Pay**: The assistant will always check your wallet balance before attempting to pay an invoice to prevent failed transactions. +4. **Invoice + QR Output**: When generating an invoice, the assistant will **ALWAYS** provide: + * The `payment_request` text string for easy copying. + * An `IMAGE:` link to the generated QR code file on a single line, allowing direct display of the QR code. + +## Installation + +This skill requires the `qrcode[pil]` Python library. + +1. **Install ClawHub CLI**: + ```bash + npm i -g clawhub + ``` +2. **Install the Skill**: + ```bash + clawhub install lnbits + ``` + +## Configuration + +The skill uses environment variables for LNbits API credentials. It's recommended to add these to your OpenClaw configuration or your agent's `.env` file. + +- `LNBITS_BASE_URL`: The base URL of your LNbits instance (e.g., `https://legend.lnbits.com` or your self-hosted URL). +- `LNBITS_API_KEY`: Your LNbits wallet's Admin Key. + +Example `.env` entries: +```dotenv +export LNBITS_BASE_URL=https://legend.lnbits.com +export LNBITS_API_KEY=YOUR_ADMIN_KEY_HERE +``` + +## Usage Examples + +Here's how you can use the `lnbits` skill with your OpenClaw assistant: + +### 0. Setup / Create Wallet + +If you don't have an LNbits wallet, you can create one (defaults to the demo server for ease of setup): + +``` +(User): Create a new lnbits wallet named "My OpenClaw Wallet" +``` +The create command prints your new `adminkey` and `base_url` in the terminal output. Copy those from the command output and save them securely in your environment variables (e.g. `.env`). The assistant will not repeat or display the adminkey in chat. + +### 1. Check Balance + +Ask your assistant for the current balance: + +``` +(User): What's my lnbits balance? +``` + +### 2. Create Invoice (Receive Funds) + +Generate an invoice for receiving funds: + +``` +(User): Create an invoice for 5000 sats for "Coffee" +``` + +The assistant will provide the Bolt11 invoice string and a QR code image. + +### 2b. Generate QR Code from Existing Invoice + +If you have a Bolt11 string and need a QR code: + +``` +(User): Generate a QR code for this invoice: lnbc1u1p... +``` + +### 3. Pay Invoice (Send Funds) + +To pay an invoice, the assistant will first decode it and then ask for confirmation: + +``` +(User): Pay this invoice: lnbc1u1p... +``` +The assistant will then prompt: "I am about to send **[Amount] sats** to **[Memo/Dest]**. Proceed? (y/n)" + +## Error Handling + +The skill is designed to catch and summarize API errors from LNbits, providing clear feedback to the user without exposing raw technical details or stack traces. + +--- + +**Developed for OpenClaw - The AI Orchestration Layer** diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..7fa5c13 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,93 @@ +--- +name: lnbits +description: "็ฎก็†LNbits้—ช็”ต้’ฑๅŒ…๏ผˆไฝ™้ขใ€ๆ”ฏไป˜ใ€ๅ‘็ฅจ๏ผ‰ใ€‚" +homepage: https://lnbits.com +metadata: {"clawdbot":{"emoji":"โšก","requires":{"bins":["python3"],"pip":["qrcode[pil]"],"env":["LNBITS_API_KEY", "LNBITS_BASE_URL"]},"primaryEnv":"LNBITS_API_KEY"}} +--- + +# LNbits Wallet Manager + +Enable the assistant to safely and effectively manage an LNbits Lightning Network wallet. + +## ๐Ÿ›‘ CRITICAL PROTOCOLS ๐Ÿ›‘ + +1. **NEVER Expose Secrets**: Do NOT display Admin Keys, User IDs, or Wallet IDs. +2. **Explicit Confirmation**: You MUST ask for "Yes/No" confirmation before paying. + * *Format*: "I am about to send **[Amount] sats** to **[Memo/Dest]**. Proceed? (y/n)" +3. **Check Balance First**: Always call `balance` before `pay` to prevent errors. +4. **ALWAYS Include Invoice + QR**: When generating an invoice, you MUST: (a) show the `payment_request` text for copying, and (b) output `MEDIA:` followed by the `qr_file` path on ONE line. NEVER skip this. + +## Usage + +### 0. Setup / Create Wallet +If the user does not have an LNbits wallet, you can create one for them on the demo server. + +```bash +python3 {baseDir}/scripts/lnbits_cli.py create --name "My Wallet" +``` + +**Action**: +1. Run the command. The CLI prints JSON containing `adminkey` and `base_url` to stdout (visible in the terminal). +2. **NEVER Expose Secrets (applies here)**: Do NOT repeat, quote, or display the `adminkey` or any secret from the output in your chat response. The user sees the command output in their terminal; that is the only place the key should appear. +3. Instruct the user in plain language only, e.g.: + > "A new wallet was created. The command output above contains your **adminkey** and **base_url**. Copy those values from the terminal and add them to your configuration or `.env` as `LNBITS_API_KEY` and `LNBITS_BASE_URL`. Do not paste the adminkey here or in any chat." + +### 1. Check Balance +Get the current wallet balance in Satoshis. + +```bash +python3 {baseDir}/scripts/lnbits_cli.py balance +``` + +### 2. Create Invoice (Receive) +Generate a Bolt11 invoice to receive funds. **QR code is always included by default.** +* **amount**: Amount in Satoshis (Integer). +* **memo**: Optional description. +* **--no-qr**: Skip QR code generation (if not needed). + +```bash +# Invoice with QR code (default) +python3 {baseDir}/scripts/lnbits_cli.py invoice --amount 1000 --memo "Pizza" + +# Invoice without QR code +python3 {baseDir}/scripts/lnbits_cli.py invoice --amount 1000 --memo "Pizza" --no-qr +``` + +**โš ๏ธ MANDATORY RESPONSE FORMAT**: When generating an invoice, your response MUST include: + +1. **Invoice text for copying**: Show the full `payment_request` string so user can copy it +2. **QR code image**: Output `MEDIA:` followed by the `qr_file` path on ONE line + +**EXACT FORMAT** (follow precisely): +``` +Here is your 100 sat invoice: + +lnbc1u1p5abc123... + +MEDIA:./clawd/.lnbits_qr/invoice_xxx.png +``` + +**CRITICAL**: The `MEDIA:` and file path MUST be on the SAME LINE. This sends the QR code image to the user. + +### 2b. Generate QR Code from Existing Invoice +Convert any Bolt11 string to a QR code image file. + +```bash +python3 {baseDir}/scripts/lnbits_cli.py qr +``` + +Returns: `{"qr_file": "./.lnbits_qr/invoice_xxx.png", "bolt11": "..."}` + +### 3. Pay Invoice (Send) +**โš ๏ธ REQUIRES CONFIRMATION**: Decode first, verify balance, ask user, then execute. + +```bash +# Step 1: Decode to verify amount/memo +python3 {baseDir}/scripts/lnbits_cli.py decode + +# Step 2: Pay (Only after user CONFIRMS) +python3 {baseDir}/scripts/lnbits_cli.py pay +``` + +## Error Handling +If the CLI returns a JSON error (e.g., `{"error": "Insufficient funds"}`), summarize it clearly for the user. Do not show raw stack traces. \ No newline at end of file diff --git a/_meta.json b/_meta.json new file mode 100644 index 0000000..afc18c2 --- /dev/null +++ b/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn72w6ykh3h1j23s9eagy1cpdd80ppvx", + "slug": "lnbits-with-qrcode", + "version": "1.0.2", + "publishedAt": 1770509935863 +} \ No newline at end of file diff --git a/scripts/lnbits_cli.py b/scripts/lnbits_cli.py new file mode 100644 index 0000000..34f5979 --- /dev/null +++ b/scripts/lnbits_cli.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +import argparse +import base64 +import io +import json +import os +import sys +import time +import urllib.request +import urllib.error + +try: + import qrcode + HAS_QRCODE = True +except ImportError: + HAS_QRCODE = False + +# Configuration +BASE_URL = os.getenv("LNBITS_BASE_URL", "https://legend.lnbits.com").rstrip("/") +API_KEY = os.getenv("LNBITS_API_KEY") +# Save QR codes relative to CWD so the path can be expressed as ./ for media parsers. +# The CWD is shared between the Python subprocess and the parent Node.js process. +QR_DIR = os.path.join(os.getcwd(), ".lnbits_qr") +QR_MAX_AGE_SECONDS = 300 # 5 minutes + +# --- Helpers --- + +def error(msg, code=1): + print(json.dumps({"error": msg})) + sys.exit(code) + +def cleanup_old_qr_files(): + """Remove QR code files older than QR_MAX_AGE_SECONDS.""" + if not os.path.exists(QR_DIR): + return + now = time.time() + for filename in os.listdir(QR_DIR): + if not filename.endswith(".png"): + continue + filepath = os.path.join(QR_DIR, filename) + try: + if now - os.path.getmtime(filepath) > QR_MAX_AGE_SECONDS: + os.remove(filepath) + except OSError: + pass + +def get_qr_path(): + """Get a unique path for a new QR code file. Returns (absolute_path, media_path). + + The media_path is a ./ relative path resolved from the gateway's CWD (home dir) + since OpenClaw's MEDIA: parser only accepts ./relative paths. + """ + os.makedirs(QR_DIR, exist_ok=True) + cleanup_old_qr_files() + filename = f"invoice_{int(time.time() * 1000)}.png" + abs_path = os.path.join(QR_DIR, filename) + media_path = "./" + os.path.relpath(abs_path, os.path.expanduser("~")) + return abs_path, media_path + +def generate_qr(data, output_path=None): + """Generate a QR code. If output_path provided, saves to file and returns path. Otherwise returns base64.""" + if not HAS_QRCODE: + return None + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_M, + box_size=10, + border=4, + ) + qr.add_data(data.upper()) # BOLT11 should be uppercase for QR + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + + if output_path: + img.save(output_path, format="PNG") + return output_path + else: + buffer = io.BytesIO() + img.save(buffer, format="PNG") + buffer.seek(0) + return base64.b64encode(buffer.read()).decode("utf-8") + +def request(method, endpoint, data=None): + if not API_KEY: + error("LNBITS_API_KEY environment variable is not set.") + + url = f"{BASE_URL}/api/v1{endpoint}" + headers = { + "X-Api-Key": API_KEY, + "Content-Type": "application/json", + "User-Agent": "LNbits-CLI/1.0" + } + body = json.dumps(data).encode("utf-8") if data else None + + req = urllib.request.Request(url, method=method, headers=headers, data=body) + try: + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as e: + error_body = e.read().decode("utf-8", errors="replace") + error(f"API Error ({e.code}): {error_body}") + except Exception as e: + error(f"Network Error: {str(e)}") + +# --- Core Logic --- + +def get_balance(): + data = request("GET", "/wallet") + return { + "name": data.get("name"), + "balance_msat": data.get("balance"), + "balance_sats": int(data.get("balance", 0) / 1000) + } + +def create_invoice(amount, memo, include_qr=True): + result = request("POST", "/payments", { + "out": False, "amount": amount, "memo": memo, "unit": "sat" + }) + if include_qr and result.get("payment_request"): + abs_path, rel_path = get_qr_path() + qr_result = generate_qr(result["payment_request"], abs_path) + if qr_result: + result["qr_file"] = rel_path + # Also include base64 for direct embedding + with open(abs_path, "rb") as f: + b64 = base64.b64encode(f.read()).decode("utf-8") + result["qr_base64"] = f"image/png;base64,{b64}" + else: + result["qr_error"] = "qrcode library not installed (pip install qrcode[pil])" + return result + +def pay_invoice(bolt11): + return request("POST", "/payments", {"out": True, "bolt11": bolt11}) + +def decode_invoice(bolt11): + return request("POST", "/payments/decode", {"data": bolt11}) + +def create_wallet(name): + url = f"{BASE_URL}/api/v1/account" + req = urllib.request.Request( + url, + method="POST", + headers={"Content-Type": "application/json", "User-Agent": "LNbits-CLI/1.0"}, + data=json.dumps({"name": name}).encode("utf-8") + ) + with urllib.request.urlopen(req, timeout=20) as resp: + return json.loads(resp.read().decode("utf-8")) + +# --- CLI Handlers --- + +def cmd_balance(args): + print(json.dumps(get_balance(), indent=2)) + +def cmd_create(args): + print(json.dumps(create_wallet(args.name), indent=2)) + +def cmd_invoice(args): + print(json.dumps(create_invoice(args.amount, args.memo, not args.no_qr), indent=2)) + +def cmd_pay(args): + print(json.dumps(pay_invoice(args.bolt11), indent=2)) + +def cmd_decode(args): + print(json.dumps(decode_invoice(args.bolt11), indent=2)) + +def cmd_qr(args): + """Generate QR code from a BOLT11 string.""" + if args.output: + abs_path = args.output + rel_path = args.output + else: + abs_path, rel_path = get_qr_path() + qr_result = generate_qr(args.bolt11, abs_path) + if qr_result: + result = {"qr_file": rel_path, "bolt11": args.bolt11} + with open(abs_path, "rb") as f: + b64 = base64.b64encode(f.read()).decode("utf-8") + result["qr_base64"] = f"image/png;base64,{b64}" + print(json.dumps(result, indent=2)) + else: + error("qrcode library not installed (pip install qrcode[pil])") + +# --- Main --- + +def main(): + parser = argparse.ArgumentParser(description="LNbits CLI Bridge for Clawdbot") + subparsers = parser.add_subparsers(dest="command", required=True) + + # Balance + p_balance = subparsers.add_parser("balance", help="Get wallet balance") + p_balance.set_defaults(func=cmd_balance) + + # Create Wallet (Account) + p_create = subparsers.add_parser("create", help="Create a new LNbits wallet") + p_create.add_argument("--name", type=str, default="Moltbot Wallet", help="Name of the new wallet") + p_create.set_defaults(func=cmd_create) + + # Invoice + p_invoice = subparsers.add_parser("invoice", help="Create a lightning invoice") + p_invoice.add_argument("--amount", type=int, required=True, help="Amount in satoshis") + p_invoice.add_argument("--memo", type=str, default="", help="Optional memo") + p_invoice.add_argument("--no-qr", action="store_true", dest="no_qr", help="Skip QR code generation") + p_invoice.set_defaults(func=cmd_invoice) + + # Pay + p_pay = subparsers.add_parser("pay", help="Pay a lightning invoice") + p_pay.add_argument("bolt11", type=str, help="The Bolt11 invoice string") + p_pay.set_defaults(func=cmd_pay) + + # Decode + p_decode = subparsers.add_parser("decode", help="Decode a lightning invoice") + p_decode.add_argument("bolt11", type=str, help="The Bolt11 invoice string") + p_decode.set_defaults(func=cmd_decode) + + # QR Code + p_qr = subparsers.add_parser("qr", help="Generate QR code from a BOLT11 invoice") + p_qr.add_argument("bolt11", type=str, help="The Bolt11 invoice string") + p_qr.add_argument("-o", "--output", type=str, help="Output PNG file path (default: auto-generated temp file)") + p_qr.set_defaults(func=cmd_qr) + + args = parser.parse_args() + + try: + args.func(args) + except Exception as e: + error(str(e)) + +if __name__ == "__main__": + main() \ No newline at end of file