Initial commit with translated description
This commit is contained in:
106
README.md
Normal file
106
README.md
Normal file
@@ -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**
|
||||||
93
SKILL.md
Normal file
93
SKILL.md
Normal file
@@ -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 <bolt11_string>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 <bolt11_string>
|
||||||
|
|
||||||
|
# Step 2: Pay (Only after user CONFIRMS)
|
||||||
|
python3 {baseDir}/scripts/lnbits_cli.py pay <bolt11_string>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn72w6ykh3h1j23s9eagy1cpdd80ppvx",
|
||||||
|
"slug": "lnbits-with-qrcode",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"publishedAt": 1770509935863
|
||||||
|
}
|
||||||
229
scripts/lnbits_cli.py
Normal file
229
scripts/lnbits_cli.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user