-
Notifications
You must be signed in to change notification settings - Fork 128
Expand file tree
/
Copy pathloopout.go
More file actions
355 lines (320 loc) · 9.59 KB
/
loopout.go
File metadata and controls
355 lines (320 loc) · 9.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package main
import (
"context"
"encoding/hex"
"fmt"
"math"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli/v3"
)
var (
channelFlag = &cli.StringFlag{
Name: "channel",
Usage: "the comma-separated list of short " +
"channel IDs of the channels to loop out",
}
)
var loopOutCommand = &cli.Command{
Name: "out",
Usage: "perform an off-chain to on-chain swap (looping out)",
ArgsUsage: "amt [addr]",
Description: `
Attempts to loop out the target amount into either the backing lnd's
wallet, or a targeted address.
The amount is to be specified in satoshis.
Optionally a BASE58/bech32 encoded bitcoin destination address may be
specified. If not specified, a new wallet address will be generated.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "addr",
Usage: "the optional address that the looped out funds " +
"should be sent to, if let blank the funds " +
"will go to lnd's wallet",
},
&cli.StringFlag{
Name: "account",
Usage: "the name of the account to generate a new " +
"address from. You can list the names of " +
"valid accounts in your backing lnd " +
"instance with \"lncli wallet accounts list\"",
Value: "",
},
&cli.StringFlag{
Name: "account_addr_type",
Usage: "the address type of the extended public key " +
"specified in account. Currently only " +
"pay-to-taproot-pubkey(p2tr) is supported",
Value: "p2tr",
},
&cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis to loop out. To check " +
"for the minimum and maximum amounts to loop " +
"out please consult \"loop terms\"",
},
&cli.Uint64Flag{
Name: "htlc_confs",
Usage: "the number of confirmations (in blocks) " +
"that we require for the htlc extended by " +
"the server before we reveal the preimage",
Value: uint64(loopdb.DefaultLoopOutHtlcConfirmations),
},
&cli.Uint64Flag{
Name: "conf_target",
Usage: "the number of blocks from the swap " +
"initiation height that the on-chain HTLC " +
"should be swept within",
Value: uint64(loop.DefaultSweepConfTarget),
},
&cli.Int64Flag{
Name: "max_swap_routing_fee",
Usage: "the max off-chain swap routing fee in " +
"satoshis, if not specified, a default max " +
"fee will be used",
},
&cli.BoolFlag{
Name: "fast",
Usage: "indicate you want to swap immediately, " +
"paying potentially a higher fee. If not " +
"set the swap server might choose to wait up " +
"to 30 minutes before publishing the swap " +
"HTLC on-chain, to save on its chain fees. " +
"Not setting this flag therefore might " +
"result in a lower swap fee",
},
&cli.DurationFlag{
Name: "payment_timeout",
Usage: "the timeout for each individual off-chain " +
"payment attempt. If not set, the default " +
"timeout of 1 hour will be used. As the " +
"payment might be retried, the actual total " +
"time may be longer",
},
&cli.StringFlag{
Name: "asset_id",
Usage: "the asset ID of the asset to loop out, " +
"if this is set, the loop daemon will " +
"require a connection to a taproot assets " +
"daemon",
},
&cli.StringFlag{
Name: "asset_edge_node",
Usage: "the pubkey of the edge node of the asset to " +
"loop out, this is required if the taproot " +
"assets daemon has multiple channels of the " +
"given asset id with different edge nodes",
},
forceFlag,
labelFlag,
verboseFlag,
channelFlag,
},
Commands: []*cli.Command{
sweepHtlcCommand,
},
Action: loopOut,
}
func loopOut(ctx context.Context, cmd *cli.Command) error {
args := cmd.Args()
var (
amtStr string
remaining []string
)
switch {
case cmd.IsSet("amt"):
amtStr = strconv.FormatUint(cmd.Uint64("amt"), 10)
case cmd.NArg() == 1 || cmd.NArg() == 2:
amtStr = args.First()
remaining = args.Tail()
default:
// Show command help if no arguments and flags were provided.
return showCommandHelp(ctx, cmd)
}
amt, err := parseAmt(amtStr)
if err != nil {
return err
}
// Parse outgoing channel set. Don't string split if the flag is empty.
// Otherwise, strings.Split returns a slice of length one with an empty
// element.
var outgoingChanSet []uint64
if cmd.IsSet("channel") {
if cmd.IsSet("asset_id") {
return fmt.Errorf("channel flag is not supported when " +
"looping out assets")
}
chanStrings := strings.Split(cmd.String("channel"), ",")
for _, chanString := range chanStrings {
chanID, err := strconv.ParseUint(chanString, 10, 64)
if err != nil {
return fmt.Errorf("error parsing channel id "+
"\"%v\"", chanString)
}
outgoingChanSet = append(outgoingChanSet, chanID)
}
}
// Validate our label early so that we can fail before getting a quote.
label := cmd.String(labelFlag.Name)
if err := labels.Validate(label); err != nil {
return err
}
if cmd.IsSet("addr") && cmd.IsSet("account") {
return fmt.Errorf("cannot set --addr and --account at the " +
"same time. Please specify only one source for a new " +
"address to sweep the loop amount to")
}
var (
destAddr string
account string
)
switch {
case cmd.IsSet("addr"):
destAddr = cmd.String("addr")
case cmd.IsSet("account"):
account = cmd.String("account")
case len(remaining) > 0:
destAddr = remaining[0]
}
if cmd.IsSet("account") != cmd.IsSet("account_addr_type") {
return fmt.Errorf("cannot set account without specifying " +
"account address type and vice versa")
}
var accountAddrType looprpc.AddressType
if cmd.IsSet("account_addr_type") {
switch cmd.String("account_addr_type") {
case "p2tr":
accountAddrType = looprpc.AddressType_TAPROOT_PUBKEY
default:
return fmt.Errorf("unknown account address type")
}
}
var assetLoopOutInfo *looprpc.AssetLoopOutRequest
var assetId []byte
if cmd.IsSet("asset_id") {
if !cmd.IsSet("asset_edge_node") {
return fmt.Errorf("asset edge node is required when " +
"assetid is set")
}
assetId, err = hex.DecodeString(cmd.String("asset_id"))
if err != nil {
return err
}
assetEdgeNode, err := hex.DecodeString(
cmd.String("asset_edge_node"),
)
if err != nil {
return err
}
assetLoopOutInfo = &looprpc.AssetLoopOutRequest{
AssetId: assetId,
AssetEdgeNode: assetEdgeNode,
}
}
client, cleanup, err := getClient(cmd)
if err != nil {
return err
}
defer cleanup()
// Set our maximum swap wait time. If a fast swap is requested we set
// it to now, otherwise to 30 minutes in the future.
fast := cmd.Bool("fast")
swapDeadline := cliClock.Now()
if !fast {
swapDeadline = cliClock.Now().Add(defaultSwapWaitTime)
}
sweepConfTarget := int32(cmd.Uint64("conf_target"))
htlcConfs := int32(cmd.Uint64("htlc_confs"))
if htlcConfs == 0 {
return fmt.Errorf("at least 1 confirmation required for htlcs")
}
quoteReq := &looprpc.QuoteRequest{
Amt: int64(amt),
ConfTarget: sweepConfTarget,
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
AssetInfo: assetLoopOutInfo,
}
quote, err := client.LoopOutQuote(ctx, quoteReq)
if err != nil {
return err
}
// Show a warning if a slow swap was requested.
var warning string
if fast {
warning = "Fast swap requested."
} else {
warning = fmt.Sprintf("Regular swap speed requested, it "+
"might take up to %v for the swap to be executed.",
defaultSwapWaitTime)
}
limits := getOutLimits(amt, quote)
// If configured, use the specified maximum swap routing fee.
if cmd.IsSet("max_swap_routing_fee") {
limits.maxSwapRoutingFee = btcutil.Amount(
cmd.Int64("max_swap_routing_fee"),
)
}
// Skip showing details if configured
if !(cmd.Bool("force") || cmd.Bool("f")) {
err = displayOutDetails(
limits, warning, quoteReq, quote, cmd.Bool("verbose"),
)
if err != nil {
return err
}
}
var paymentTimeout int64
if cmd.IsSet("payment_timeout") {
parsedTimeout := cmd.Duration("payment_timeout")
if parsedTimeout.Truncate(time.Second) != parsedTimeout {
return fmt.Errorf("payment timeout must be a " +
"whole number of seconds")
}
paymentTimeout = int64(parsedTimeout.Seconds())
if paymentTimeout <= 0 {
return fmt.Errorf("payment timeout must be a " +
"positive value")
}
if paymentTimeout > math.MaxUint32 {
return fmt.Errorf("payment timeout is too large")
}
}
resp, err := client.LoopOut(ctx, &looprpc.LoopOutRequest{
Amt: int64(amt),
Dest: destAddr,
IsExternalAddr: destAddr != "",
Account: account,
AccountAddrType: accountAddrType,
MaxMinerFee: int64(limits.maxMinerFee),
MaxPrepayAmt: int64(limits.maxPrepayAmt),
MaxSwapFee: int64(limits.maxSwapFee),
MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee),
OutgoingChanSet: outgoingChanSet,
SweepConfTarget: sweepConfTarget,
HtlcConfirmations: htlcConfs,
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
Label: label,
Initiator: defaultInitiator,
PaymentTimeout: uint32(paymentTimeout),
AssetInfo: assetLoopOutInfo,
AssetRfqInfo: quote.AssetRfqInfo,
})
if err != nil {
return err
}
fmt.Printf("Swap initiated\n")
fmt.Printf("ID: %x\n", resp.IdBytes)
if resp.ServerMessage != "" {
fmt.Printf("Server message: %v\n", resp.ServerMessage)
}
fmt.Println()
fmt.Printf("Run `loop monitor` to monitor progress.\n")
return nil
}