Skip to content

Commit 75f1482

Browse files
committed
Wait for payment settlement, LNURL support, --no-wait flag
- `pay` now waits for settlement via SSE (like `invoice create`) - `pay` accepts LNURL targets alongside Lightning addresses and BOLT11 invoices - Add `--no-wait` flag to both `pay` and `invoice create` to return immediately - `invoice create --json` now waits for settlement by default (use --no-wait to skip) - Upgrade go-sdk to v0.3.0 - Bump version to 0.3.0 Made-with: Cursor
1 parent 37e0c7b commit 75f1482

5 files changed

Lines changed: 138 additions & 41 deletions

File tree

‎cmd/invoice.go‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func init() {
2626
invoiceCreateCmd.Flags().Int64("amount", 0, "amount in sats (required)")
2727
invoiceCreateCmd.MarkFlagRequired("amount")
2828
invoiceCreateCmd.Flags().String("memo", "", "short description attached to the invoice")
29+
invoiceCreateCmd.Flags().Bool("no-wait", false, "return immediately without waiting for payment")
2930

3031
invoiceListCmd.Flags().Int("limit", 20, "max number of results")
3132
invoiceListCmd.Flags().Int("after", 0, "show results after this invoice number (for pagination)")
@@ -39,11 +40,12 @@ var invoiceCreateCmd = &cobra.Command{
3940
Short: "Create a Lightning invoice to receive sats",
4041
Long: `Create a new Lightning invoice for the given amount.
4142
42-
Prints the BOLT11 string, renders a QR code in the terminal, and
43-
automatically waits for the payment to settle via SSE. Press Ctrl+C
44-
to stop waiting — the invoice remains valid until it expires.`,
43+
Prints the BOLT11 string and automatically waits for the payment to
44+
settle via SSE. Use --no-wait to return immediately. The invoice
45+
remains valid until it expires.`,
4546
Example: ` lnbot invoice create --amount 1000
4647
lnbot invoice create --amount 5000 --memo "for coffee"
48+
lnbot invoice create --amount 100 --no-wait
4749
lnbot invoice create --amount 100 --json`,
4850
RunE: func(cmd *cobra.Command, args []string) error {
4951
if err := requireConfig(); err != nil {
@@ -73,8 +75,27 @@ to stop waiting — the invoice remains valid until it expires.`,
7375
return apiError("creating invoice", err)
7476
}
7577

78+
noWait, _ := cmd.Flags().GetBool("no-wait")
79+
7680
if jsonFlag {
77-
return json.NewEncoder(os.Stdout).Encode(invoice)
81+
if noWait {
82+
return json.NewEncoder(os.Stdout).Encode(invoice)
83+
}
84+
events, errs := ln.Invoices.Watch(ctx, invoice.Number, nil)
85+
for {
86+
select {
87+
case ev, ok := <-events:
88+
if !ok {
89+
return json.NewEncoder(os.Stdout).Encode(invoice)
90+
}
91+
return json.NewEncoder(os.Stdout).Encode(ev.Data)
92+
case err, ok := <-errs:
93+
if ok && err != nil {
94+
return json.NewEncoder(os.Stdout).Encode(invoice)
95+
}
96+
return json.NewEncoder(os.Stdout).Encode(invoice)
97+
}
98+
}
7899
}
79100

80101
fmt.Printf(" amount: %s\n", format.Sats(invoice.Amount))
@@ -83,6 +104,10 @@ to stop waiting — the invoice remains valid until it expires.`,
83104
fmt.Printf(" %s\n", invoice.Bolt11)
84105
fmt.Println()
85106

107+
if noWait {
108+
return nil
109+
}
110+
86111
fmt.Print(" Waiting for payment... (Ctrl+C to stop)")
87112

88113
watchCtx, cancel := context.WithCancel(ctx)

‎cmd/pay.go‎

Lines changed: 105 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,28 @@ import (
1616
)
1717

1818
var payCmd = &cobra.Command{
19-
Use: "pay <address-or-bolt11>",
20-
Short: "Send sats to a Lightning address or BOLT11 invoice",
19+
Use: "pay <target>",
20+
Short: "Send sats to a Lightning address, LNURL, or BOLT11 invoice",
2121
Long: `Send sats via the Lightning Network. The target can be:
2222
2323
- A Lightning address (user@domain) — requires --amount
24+
- An LNURL (lnurl1...) — requires --amount
2425
- A BOLT11 invoice (starts with lnbc/lntb/lnbs) — amount is encoded
2526
26-
A confirmation prompt is shown before sending. Use --yes to skip it.`,
27+
A confirmation prompt is shown before sending. Use --yes to skip it.
28+
The CLI waits for settlement via SSE. Use --no-wait to return immediately.`,
2729
Example: ` # Pay a Lightning address
2830
lnbot pay alice@ln.bot --amount 1000
2931
3032
# Pay a BOLT11 invoice (amount is in the invoice)
3133
lnbot pay lnbc10u1pj9x...
3234
35+
# Pay an LNURL
36+
lnbot pay lnurl1dp68gurn8ghj7... --amount 500
37+
38+
# Return immediately without waiting for settlement
39+
lnbot pay alice@ln.bot --amount 500 --no-wait
40+
3341
# Skip the confirmation prompt
3442
lnbot pay alice@ln.bot --amount 500 --yes`,
3543
Args: cobra.ExactArgs(1),
@@ -46,22 +54,23 @@ A confirmation prompt is shown before sending. Use --yes to skip it.`,
4654
strings.HasPrefix(lower, "lntb") ||
4755
strings.HasPrefix(lower, "lnbs")
4856
isAddress := strings.Contains(target, "@")
57+
isLNURL := strings.HasPrefix(lower, "lnurl")
4958

5059
amount, _ := cmd.Flags().GetInt64("amount")
5160
maxFee, _ := cmd.Flags().GetInt64("max-fee")
5261

5362
if amount > 0 {
5463
params.Amount = lnbot.Ptr(amount)
55-
} else if isAddress {
56-
return fmt.Errorf("--amount is required when paying a Lightning address\n\n lnbot pay %s --amount <sats>", target)
64+
} else if isAddress || isLNURL {
65+
return fmt.Errorf("--amount is required when paying a Lightning address or LNURL\n\n lnbot pay %s --amount <sats>", format.Truncate(target, 40))
5766
}
5867

5968
if maxFee > 0 {
6069
params.MaxFee = lnbot.Ptr(maxFee)
6170
}
6271

63-
if !isBolt11 && !isAddress {
64-
return fmt.Errorf("unrecognized target: %s\n\nTarget must be a Lightning address (user@domain) or BOLT11 invoice (lnbc...)", format.Truncate(target, 40))
72+
if !isBolt11 && !isAddress && !isLNURL {
73+
return fmt.Errorf("unrecognized target: %s\n\nTarget must be a Lightning address (user@domain), LNURL (lnurl1...), or BOLT11 invoice (lnbc...)", format.Truncate(target, 40))
6574
}
6675

6776
ln, _, _, err := cfg.Client(walletFlag)
@@ -84,47 +93,110 @@ A confirmation prompt is shown before sending. Use --yes to skip it.`,
8493
}
8594
}
8695

96+
ctx := context.Background()
8797
start := time.Now()
88-
payment, err := ln.Payments.Create(context.Background(), params)
98+
payment, err := ln.Payments.Create(ctx, params)
8999
if err != nil {
90100
return apiError("sending payment", err)
91101
}
92-
elapsed := time.Since(start)
102+
103+
noWait, _ := cmd.Flags().GetBool("no-wait")
93104

94105
if jsonFlag {
106+
if !noWait && (payment.Status == "pending" || payment.Status == "processing") {
107+
payment, err = waitForPaymentJSON(ctx, ln, payment)
108+
if err != nil {
109+
return json.NewEncoder(os.Stdout).Encode(payment)
110+
}
111+
}
95112
return json.NewEncoder(os.Stdout).Encode(payment)
96113
}
97114

98-
switch payment.Status {
99-
case "settled":
100-
if elapsed < 100*time.Millisecond {
101-
printSuccess("Sent! Settled instantly")
102-
} else {
103-
printSuccess(fmt.Sprintf("Sent! Settled in %dms", elapsed.Milliseconds()))
104-
}
105-
fmt.Printf(" amount: %s\n", format.Sats(payment.Amount))
106-
if payment.ActualFee != nil && *payment.ActualFee > 0 {
107-
fmt.Printf(" fee: %s\n", format.Sats(*payment.ActualFee))
115+
if noWait {
116+
fmt.Printf(" status: %s\n", payment.Status)
117+
fmt.Printf(" number: %d\n", payment.Number)
118+
return nil
119+
}
120+
121+
return printPaymentResult(ctx, ln, payment, start)
122+
},
123+
}
124+
125+
func printPaymentResult(ctx context.Context, ln *lnbot.Client, payment *lnbot.Payment, start time.Time) error {
126+
switch payment.Status {
127+
case "settled":
128+
elapsed := time.Since(start)
129+
if elapsed < 100*time.Millisecond {
130+
printSuccess("Sent! Settled instantly")
131+
} else {
132+
printSuccess(fmt.Sprintf("Sent! Settled in %dms", elapsed.Milliseconds()))
133+
}
134+
fmt.Printf(" amount: %s\n", format.Sats(payment.Amount))
135+
if payment.ActualFee != nil && *payment.ActualFee > 0 {
136+
fmt.Printf(" fee: %s\n", format.Sats(*payment.ActualFee))
137+
}
138+
w, err := ln.Wallets.Current(ctx)
139+
if err == nil {
140+
fmt.Printf(" balance: %s\n", format.Sats(w.Available))
141+
}
142+
case "failed":
143+
reason := "unknown"
144+
if payment.FailureReason != nil {
145+
reason = *payment.FailureReason
146+
}
147+
fmt.Fprintf(os.Stderr, "✗ Payment failed: %s\n", reason)
148+
fmt.Fprintln(os.Stderr, " No sats were deducted.")
149+
default:
150+
fmt.Print(" Waiting for settlement... (Ctrl+C to stop)")
151+
152+
watchCtx, cancel := context.WithCancel(ctx)
153+
defer cancel()
154+
155+
events, errs := ln.Payments.Watch(watchCtx, payment.Number, nil)
156+
for {
157+
select {
158+
case ev, ok := <-events:
159+
if !ok {
160+
fmt.Println()
161+
return nil
162+
}
163+
fmt.Println()
164+
return printPaymentResult(ctx, ln, &ev.Data, start)
165+
case err, ok := <-errs:
166+
if ok && err != nil {
167+
fmt.Println()
168+
return err
169+
}
170+
return nil
108171
}
109-
w, err := ln.Wallets.Current(context.Background())
110-
if err == nil {
111-
fmt.Printf(" balance: %s\n", format.Sats(w.Available))
172+
}
173+
}
174+
return nil
175+
}
176+
177+
func waitForPaymentJSON(ctx context.Context, ln *lnbot.Client, payment *lnbot.Payment) (*lnbot.Payment, error) {
178+
watchCtx, cancel := context.WithCancel(ctx)
179+
defer cancel()
180+
181+
events, errs := ln.Payments.Watch(watchCtx, payment.Number, nil)
182+
for {
183+
select {
184+
case ev, ok := <-events:
185+
if !ok {
186+
return payment, nil
112187
}
113-
case "failed":
114-
reason := "unknown"
115-
if payment.FailureReason != nil {
116-
reason = *payment.FailureReason
188+
return &ev.Data, nil
189+
case err, ok := <-errs:
190+
if ok && err != nil {
191+
return payment, err
117192
}
118-
fmt.Fprintf(os.Stderr, "✗ Payment failed: %s\n", reason)
119-
fmt.Fprintln(os.Stderr, " No sats were deducted.")
120-
default:
121-
fmt.Printf(" status: %s\n", payment.Status)
193+
return payment, nil
122194
}
123-
return nil
124-
},
195+
}
125196
}
126197

127198
func init() {
128-
payCmd.Flags().Int64("amount", 0, "amount in sats (required for Lightning addresses)")
199+
payCmd.Flags().Int64("amount", 0, "amount in sats (required for Lightning addresses and LNURLs)")
129200
payCmd.Flags().Int64("max-fee", 0, "maximum routing fee in sats")
201+
payCmd.Flags().Bool("no-wait", false, "return immediately without waiting for settlement")
130202
}

‎cmd/root.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ var (
2222
cfg *config.Config
2323
)
2424

25-
const version = "0.2.0"
25+
const version = "0.3.0"
2626

2727
var rootCmd = &cobra.Command{
2828
Use: "lnbot",

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/lnbotdev/cli
33
go 1.21
44

55
require (
6-
github.com/lnbotdev/go-sdk v0.1.0
6+
github.com/lnbotdev/go-sdk v0.3.0
77
github.com/spf13/cobra v1.10.2
88
)
99

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
22
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
33
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
4-
github.com/lnbotdev/go-sdk v0.1.0 h1:oRZXC+XBagb7peWVeT7MxSWL6IYDfMvy+Sc7JMjtcL4=
5-
github.com/lnbotdev/go-sdk v0.1.0/go.mod h1:YoerG407chXePT+92bunp4bQWF65T4UvueRI1RVueWg=
4+
github.com/lnbotdev/go-sdk v0.3.0 h1:gcZnoOHpeSzJWUsYw4UZBO8YM9WFirCwubw+RlnbmAE=
5+
github.com/lnbotdev/go-sdk v0.3.0/go.mod h1:YoerG407chXePT+92bunp4bQWF65T4UvueRI1RVueWg=
66
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
77
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
88
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=

0 commit comments

Comments
 (0)