33set -euo pipefail
44
55CLI_NAME=" $( basename $0 ) "
6+ SCRIPT_DIR=" $( cd " $( dirname " $0 " ) " && pwd) "
67BITCOIN_CONTAINER=" bitcoind"
78LND_CONTAINER=" lnd"
89LND_DIR=" /home/lnd/.lnd"
910RPC_USER=polaruser
1011RPC_PASS=polarpass
1112RPC_PORT=43782
13+ LNURL_SERVER_PORT=" ${LNURL_SERVER_PORT:- 30001} "
14+ LNURL_SERVER_URL=" ${LNURL_SERVER_URL:- http:// localhost: ${LNURL_SERVER_PORT} } "
1215
1316BASE_COMMAND=(docker compose exec $BITCOIN_CONTAINER bitcoin-cli -rpcport=$RPC_PORT -rpcuser=$RPC_USER -rpcpassword=$RPC_PASS )
1417LNCLI_CMD=(docker compose exec -T $LND_CONTAINER lncli --lnddir " $LND_DIR " --network regtest)
@@ -34,6 +37,12 @@ Commands:
3437 holdinvoice [amount] [-m memo] Create a hold invoice
3538 settleinvoice <preimage> Reveal a preimage and use it to settle the corresponding invoice
3639 cancelinvoice <payment_hash> Cancels a currently open invoice
40+ LNURL (requires LNURL server, default: ${LNURL_SERVER_URL} ):
41+ startlnurlserver [--port N] Start LNURL server using current e2e Bitcoin/LND
42+ stoplnurlserver Stop LNURL server container
43+ checklnurl Diagnose LNURL health, routing, and adb reverse
44+ getlnurlpay --msat N Create LNURL-pay with fixed msat amount
45+ getlnurlwithdraw --msat N Create LNURL-withdraw with fixed msat amount
3746EOF
3847}
3948
@@ -273,6 +282,333 @@ if [[ "$command" = "getlninvoice" ]]; then
273282 exit
274283fi
275284
285+ # Start LNURL server stack (bitkit-docker)
286+ if [[ " $command " = " startlnurlserver" ]]; then
287+ shift
288+
289+ BITKIT_DOCKER_DIR=" ${BITKIT_DOCKER_DIR:- $SCRIPT_DIR / ../ ../ bitkit-docker} "
290+ LNURL_IMAGE=" ${LNURL_IMAGE:- bitkit-lnurl-server: local} "
291+ E2E_DOCKER_DIR=" $SCRIPT_DIR "
292+ LNURL_DATA_DIR=" ${LNURL_DATA_DIR:- $SCRIPT_DIR / lnurl-server-data} "
293+ runtime_port=" $LNURL_SERVER_PORT "
294+
295+ while [[ $# -gt 0 ]]; do
296+ case " $1 " in
297+ --port)
298+ runtime_port=" ${2:- } "
299+ shift 2
300+ ;;
301+ -* )
302+ echo " Unknown option $1 "
303+ echo " Usage: $CLI_NAME startlnurlserver [--port N]"
304+ exit 1
305+ ;;
306+ * )
307+ if [[ " $1 " =~ ^[0-9]+$ ]]; then
308+ runtime_port=" $1 "
309+ shift
310+ else
311+ echo " Invalid port '$1 ' (expected integer)"
312+ exit 1
313+ fi
314+ ;;
315+ esac
316+ done
317+
318+ if ! [[ " $runtime_port " =~ ^[0-9]+$ ]]; then
319+ echo " Invalid port '$runtime_port ' (expected integer)"
320+ exit 1
321+ fi
322+
323+ if [[ ! -d " $BITKIT_DOCKER_DIR " ]]; then
324+ echo " bitkit-docker directory not found at '$BITKIT_DOCKER_DIR '"
325+ echo " Tip: set BITKIT_DOCKER_DIR to your path, e.g."
326+ echo " BITKIT_DOCKER_DIR=../bitkit-docker $CLI_NAME startlnurlserver"
327+ exit 1
328+ fi
329+
330+ if [[ ! -f " $BITKIT_DOCKER_DIR /docker-compose.yml" ]]; then
331+ echo " No docker-compose.yml found in '$BITKIT_DOCKER_DIR '"
332+ exit 1
333+ fi
334+
335+ if [[ ! -d " $E2E_DOCKER_DIR /lnd" ]]; then
336+ echo " Expected LND data at '$E2E_DOCKER_DIR /lnd' but it was not found."
337+ exit 1
338+ fi
339+
340+ if ! docker image inspect " $LNURL_IMAGE " > /dev/null 2>&1 ; then
341+ echo " → Building LNURL image '$LNURL_IMAGE ' from '$BITKIT_DOCKER_DIR /lnurl-server'..."
342+ docker build -t " $LNURL_IMAGE " " $BITKIT_DOCKER_DIR /lnurl-server" > /dev/null || {
343+ echo " Failed to build LNURL image"
344+ exit 1
345+ }
346+ fi
347+
348+ mkdir -p " $LNURL_DATA_DIR "
349+
350+ echo " → Starting LNURL server wired to current e2e docker stack on port ${runtime_port} ..."
351+ docker rm -f lnurl-server > /dev/null 2>&1 || true
352+ docker run -d \
353+ --name lnurl-server \
354+ -p " ${runtime_port} :3000" \
355+ -e NODE_ENV=production \
356+ -e PORT=3000 \
357+ -e DOMAIN=" http://localhost:${runtime_port} " \
358+ -e BITCOIN_RPC_HOST=host.docker.internal \
359+ -e BITCOIN_RPC_PORT=43782 \
360+ -e BITCOIN_RPC_USER=polaruser \
361+ -e BITCOIN_RPC_PASS=polarpass \
362+ -e LND_REST_HOST=host.docker.internal \
363+ -e LND_REST_PORT=8080 \
364+ -e LND_MACAROON_PATH=/lnd-certs/data/chain/bitcoin/regtest/admin.macaroon \
365+ -e LND_TLS_CERT_PATH=/lnd-certs/tls.cert \
366+ -v " $E2E_DOCKER_DIR /lnd:/lnd-certs:ro" \
367+ -v " $LNURL_DATA_DIR :/data" \
368+ " $LNURL_IMAGE " > /dev/null || {
369+ echo " Failed to start LNURL server container"
370+ exit 1
371+ }
372+
373+ echo " ✓ LNURL server started"
374+ echo " Health check:"
375+ echo " curl -s http://localhost:${runtime_port} /health | jq"
376+ exit
377+ fi
378+
379+ # Stop LNURL server
380+ if [[ " $command " = " stoplnurlserver" ]]; then
381+ container_id=$( docker ps -a --filter " name=^lnurl-server$" --format " {{.ID}}" | head -n 1)
382+ if [[ -n " $container_id " ]]; then
383+ docker rm -f lnurl-server > /dev/null 2>&1 || true
384+ echo " ✓ LNURL server stopped"
385+ else
386+ echo " LNURL server is not running"
387+ fi
388+ exit
389+ fi
390+
391+ # Check LNURL setup and prerequisites
392+ if [[ " $command " = " checklnurl" ]]; then
393+ health_url=" ${LNURL_SERVER_URL%/ } /health"
394+
395+ echo " → Checking LNURL health at ${health_url} ..."
396+ health_json=" "
397+ for _ in $( seq 1 10) ; do
398+ health_json=$( curl -s " $health_url " 2> /dev/null || true)
399+ if [[ -n " $health_json " ]]; then
400+ break
401+ fi
402+ sleep 1
403+ done
404+ if [[ -z " $health_json " ]]; then
405+ echo " ✗ LNURL health endpoint is unreachable"
406+ echo " Start server: $CLI_NAME startlnurlserver --port ${LNURL_SERVER_PORT} "
407+ echo " Or set LNURL_SERVER_URL to your endpoint."
408+ exit 1
409+ fi
410+
411+ status=$( echo " $health_json " | jq -r ' .status // "unknown"' 2> /dev/null || echo " unknown" )
412+ btc_ok=$( echo " $health_json " | jq -r ' .bitcoin_connected // false' 2> /dev/null || echo " false" )
413+ lnd_ok=$( echo " $health_json " | jq -r ' .lnd_connected // false' 2> /dev/null || echo " false" )
414+ block_height=$( echo " $health_json " | jq -r ' .block_height // "n/a"' 2> /dev/null || echo " n/a" )
415+
416+ echo " status: $status "
417+ echo " bitcoin_connected: $btc_ok "
418+ echo " lnd_connected: $lnd_ok "
419+ echo " block_height: $block_height "
420+
421+ echo " "
422+ echo " → Checking local LND channel/liquidity..."
423+ channel_balance=$( docker compose -f " $SCRIPT_DIR /docker-compose.yml" exec -T " $LND_CONTAINER " \
424+ lncli --lnddir " $LND_DIR " --network regtest channelbalance 2> /dev/null || true)
425+ channels=$( docker compose -f " $SCRIPT_DIR /docker-compose.yml" exec -T " $LND_CONTAINER " \
426+ lncli --lnddir " $LND_DIR " --network regtest listchannels 2> /dev/null || true)
427+
428+ if [[ -z " $channel_balance " || -z " $channels " ]]; then
429+ echo " ⚠ Could not read local LND state from e2e docker stack."
430+ else
431+ local_sat=$( echo " $channel_balance " | jq -r ' .local_balance.sat // "0"' 2> /dev/null || echo " 0" )
432+ remote_sat=$( echo " $channel_balance " | jq -r ' .remote_balance.sat // "0"' 2> /dev/null || echo " 0" )
433+ active_count=$( echo " $channels " | jq -r ' [.channels[] | select(.active==true)] | length' 2> /dev/null || echo " 0" )
434+ total_count=$( echo " $channels " | jq -r ' .channels | length // 0' 2> /dev/null || echo " 0" )
435+
436+ echo " channels: ${active_count} active / ${total_count} total"
437+ echo " lnd outbound: ${local_sat} sats (can pay)"
438+ echo " lnd inbound: ${remote_sat} sats (can receive)"
439+ fi
440+
441+ echo " "
442+ echo " → Checking Android adb reverse for port ${LNURL_SERVER_PORT} ..."
443+ if ! command -v adb > /dev/null 2>&1 ; then
444+ echo " adb not found (skip)"
445+ else
446+ devices=$( adb devices 2> /dev/null | awk ' NR>1 && $2=="device" {print $1}' )
447+ if [[ -z " $devices " ]]; then
448+ echo " no connected Android devices/emulators"
449+ else
450+ reverse_list=$( adb reverse --list 2> /dev/null || true)
451+ if echo " $reverse_list " | grep -q " tcp:${LNURL_SERVER_PORT} [[:space:]]\+tcp:${LNURL_SERVER_PORT} " ; then
452+ echo " ✓ adb reverse is configured for tcp:${LNURL_SERVER_PORT} "
453+ else
454+ echo " ⚠ adb reverse missing for tcp:${LNURL_SERVER_PORT} "
455+ echo " Run: adb reverse tcp:${LNURL_SERVER_PORT} tcp:${LNURL_SERVER_PORT} "
456+ fi
457+ fi
458+ fi
459+
460+ echo " "
461+ if [[ " $status " = " healthy" && " $btc_ok " = " true" && " $lnd_ok " = " true" ]]; then
462+ echo " ✓ LNURL diagnostics look good."
463+ exit 0
464+ fi
465+
466+ echo " ⚠ LNURL diagnostics found issues."
467+ exit 1
468+ fi
469+
470+ # Create LNURL-pay (fixed msat amount)
471+ if [[ " $command " = " getlnurlpay" ]]; then
472+ shift
473+
474+ msat=" "
475+ comment_allowed=0
476+
477+ while [[ $# -gt 0 ]]; do
478+ case " $1 " in
479+ --msat)
480+ msat=" ${2:- } "
481+ shift 2
482+ ;;
483+ --comment-allowed)
484+ comment_allowed=" ${2:- 0} "
485+ shift 2
486+ ;;
487+ -* )
488+ echo " Unknown option $1 "
489+ echo " Usage: $CLI_NAME getlnurlpay --msat N [--comment-allowed N]"
490+ exit 1
491+ ;;
492+ * )
493+ if [[ " $1 " =~ ^[0-9]+$ ]]; then
494+ msat=" $1 "
495+ shift
496+ else
497+ echo " Invalid value '$1 ' (expected integer msats)"
498+ exit 1
499+ fi
500+ ;;
501+ esac
502+ done
503+
504+ if [[ -z " $msat " ]]; then
505+ echo " Usage: $CLI_NAME getlnurlpay --msat N [--comment-allowed N]"
506+ exit 1
507+ fi
508+
509+ if ! [[ " $msat " =~ ^[0-9]+$ ]]; then
510+ echo " Invalid msat '$msat ' (expected integer)"
511+ exit 1
512+ fi
513+
514+ if ! [[ " $comment_allowed " =~ ^[0-9]+$ ]]; then
515+ echo " Invalid comment length '$comment_allowed ' (expected integer)"
516+ exit 1
517+ fi
518+
519+ url=" ${LNURL_SERVER_URL%/ } /generate/pay?minSendable=${msat} &maxSendable=${msat} &commentAllowed=${comment_allowed} "
520+ echo " → Creating LNURL-pay (${msat} msat fixed amount) via ${LNURL_SERVER_URL} ..."
521+
522+ result=$( curl -sf " $url " ) || {
523+ echo " Failed to reach LNURL server at ${LNURL_SERVER_URL} "
524+ echo " Tip: verify server with '$CLI_NAME checklnurl' or set LNURL_SERVER_URL."
525+ exit 1
526+ }
527+
528+ lnurl=$( echo " $result " | jq -r ' .lnurl // empty' 2> /dev/null) || lnurl=" "
529+ if [[ -z " $lnurl " ]]; then
530+ echo " ${result:- LNURL server returned no output} "
531+ exit 1
532+ fi
533+
534+ echo " "
535+ echo " $lnurl "
536+ echo " "
537+
538+ if command -v pbcopy & > /dev/null; then
539+ echo " $lnurl " | pbcopy
540+ echo " LNURL copied to clipboard."
541+ fi
542+
543+ exit
544+ fi
545+
546+ # Create LNURL-withdraw (fixed msat amount)
547+ if [[ " $command " = " getlnurlwithdraw" ]]; then
548+ shift
549+
550+ msat=" "
551+
552+ while [[ $# -gt 0 ]]; do
553+ case " $1 " in
554+ --msat)
555+ msat=" ${2:- } "
556+ shift 2
557+ ;;
558+ -* )
559+ echo " Unknown option $1 "
560+ echo " Usage: $CLI_NAME getlnurlwithdraw --msat N"
561+ exit 1
562+ ;;
563+ * )
564+ if [[ " $1 " =~ ^[0-9]+$ ]]; then
565+ msat=" $1 "
566+ shift
567+ else
568+ echo " Invalid value '$1 ' (expected integer msats)"
569+ exit 1
570+ fi
571+ ;;
572+ esac
573+ done
574+
575+ if [[ -z " $msat " ]]; then
576+ echo " Usage: $CLI_NAME getlnurlwithdraw --msat N"
577+ exit 1
578+ fi
579+
580+ if ! [[ " $msat " =~ ^[0-9]+$ ]]; then
581+ echo " Invalid msat '$msat ' (expected integer)"
582+ exit 1
583+ fi
584+
585+ url=" ${LNURL_SERVER_URL%/ } /generate/withdraw?minWithdrawable=${msat} &maxWithdrawable=${msat} "
586+ echo " → Creating LNURL-withdraw (${msat} msat fixed amount) via ${LNURL_SERVER_URL} ..."
587+
588+ result=$( curl -sf " $url " ) || {
589+ echo " Failed to reach LNURL server at ${LNURL_SERVER_URL} "
590+ echo " Tip: verify server with '$CLI_NAME checklnurl' or set LNURL_SERVER_URL."
591+ exit 1
592+ }
593+
594+ lnurl=$( echo " $result " | jq -r ' .lnurl // empty' 2> /dev/null) || lnurl=" "
595+ if [[ -z " $lnurl " ]]; then
596+ echo " ${result:- LNURL server returned no output} "
597+ exit 1
598+ fi
599+
600+ echo " "
601+ echo " $lnurl "
602+ echo " "
603+
604+ if command -v pbcopy & > /dev/null; then
605+ echo " $lnurl " | pbcopy
606+ echo " LNURL copied to clipboard."
607+ fi
608+
609+ exit
610+ fi
611+
276612# Open channel from LND to a node
277613if [[ " $command " = " openchannel" ]]; then
278614 shift
0 commit comments