diff --git a/skills/lnbits/SKILL.md b/skills/lnbits/SKILL.md new file mode 100644 index 000000000..9dac9a252 --- /dev/null +++ b/skills/lnbits/SKILL.md @@ -0,0 +1,64 @@ +--- +name: lnbits +description: Manage LNbits Lightning Wallet (Balance, Pay, Invoice) +homepage: https://lnbits.com +metadata: {"clawdbot":{"emoji":"⚡","requires":{"bins":["python3"],"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 SECURITY 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. + +## 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. +2. Capture the `adminkey` (Admin Key) and `base_url` (defaults to https://demo.lnbits.com). +3. **IMPORTANT**: Instruct the user to save these credentials securely: + > "I've created a new wallet! Please add these to your Moltbot configuration or `.env` file: + > `export LNBITS_BASE_URL=https://demo.lnbits.com` + > `export LNBITS_API_KEY=`" + +### 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. +* **amount**: Amount in Satoshis (Integer). +* **memo**: Optional description. + +```bash +python3 {baseDir}/scripts/lnbits_cli.py invoice --amount 1000 --memo "Pizza" +``` + +### 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/skills/lnbits/scripts/lnbits_cli.py b/skills/lnbits/scripts/lnbits_cli.py new file mode 100755 index 000000000..89c7a5e39 --- /dev/null +++ b/skills/lnbits/scripts/lnbits_cli.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import sys +import urllib.request +import urllib.error + +# Configuration +BASE_URL = os.getenv("LNBITS_BASE_URL", "https://legend.lnbits.com").rstrip("/") +API_KEY = os.getenv("LNBITS_API_KEY") + +# --- Helpers --- + +def error(msg, code=1): + print(json.dumps({"error": msg})) + sys.exit(code) + +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" + } + 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): + return request("POST", "/payments", { + "out": False, "amount": amount, "memo": memo, "unit": "sat" + }) + +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"}, + 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), 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)) + +# --- 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.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) + + 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