github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/cmd/lit-af/shell.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "sort" 7 "strconv" 8 9 "io/ioutil" 10 "net/http" 11 12 "github.com/fatih/color" 13 "github.com/mit-dci/lit/coinparam" 14 "github.com/mit-dci/lit/consts" 15 "github.com/mit-dci/lit/litrpc" 16 "github.com/mit-dci/lit/lnutil" 17 ) 18 19 var lsCommand = &Command{ 20 Format: fmt.Sprintf("%s%s\n", lnutil.White("ls"), lnutil.ReqColor(("topic"))), 21 Description: fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", 22 "Show various information about our current state, such as connections, addresses, UTXO's, balances, etc.", 23 fmt.Sprintf("%s %s", 24 lnutil.White("topic"), 25 "What information to show. Provide one of:"), 26 fmt.Sprintf("\t%-20s %s", 27 lnutil.White("-a"), 28 "Information on connections to other peers"), 29 fmt.Sprintf("\t%-20s %s", 30 lnutil.White("conns"), 31 "Information on connections to other peers"), 32 fmt.Sprintf("\t%-20s %s", 33 lnutil.White("chans"), 34 "Information on payment channels"), 35 fmt.Sprintf("\t%-20s %s", 36 lnutil.White("dualfunds"), 37 "Information on pending dual funding requests"), 38 fmt.Sprintf("\t%-20s %s", 39 lnutil.White("txos"), 40 "Information on unspent outputs"), 41 fmt.Sprintf("\t%-20s %s", 42 lnutil.White("ports"), 43 "Information on listening addresses/ports"), 44 fmt.Sprintf("\t%-20s %s", 45 lnutil.White("addrs"), 46 "Information on blockchain addresses"), 47 fmt.Sprintf("\t%-20s %s", 48 lnutil.White("bals"), 49 "Information on wallet balances")), 50 ShortDescription: "Show various information about our current state\n", 51 } 52 53 var exitCommand = &Command{ 54 Format: lnutil.White("exit\n"), 55 Description: fmt.Sprintf("Alias: %s\nExit the interactive shell.\n", lnutil.White("quit")), 56 ShortDescription: fmt.Sprintf("Alias: %s\nExit the interactive shell.\n", lnutil.White("quit")), 57 } 58 59 var helpCommand = &Command{ 60 Format: fmt.Sprintf("%s%s\n", lnutil.White("help"), lnutil.OptColor("command")), 61 Description: "Show information about a given command\n", 62 ShortDescription: "Show information about a given command\n", 63 } 64 65 var offCommand = &Command{ 66 Format: lnutil.White("off"), 67 Description: "Shut down the lit node.\n", 68 ShortDescription: "Shut down the lit node.\n", 69 } 70 71 func parseErr(err error, str string) error { 72 if err != nil { 73 fmt.Fprintf(color.Output, "%s error: %s\n", str, err) 74 } 75 return nil 76 } 77 78 // Shellparse parses user input and hands it to command functions if matching 79 func (lc *litAfClient) Shellparse(cmdslice []string) error { 80 var err error 81 var args []string 82 cmd := cmdslice[0] 83 if len(cmdslice) > 1 { 84 args = cmdslice[1:] 85 } 86 87 if cmd == "exit" || cmd == "quit" { 88 return lc.Exit(args) 89 } 90 // help gives you really terse help. Just a list of commands. 91 if cmd == "help" { 92 err = lc.Help(args) 93 return parseErr(err, "help") 94 } 95 96 if cmd == "watch" { 97 err = lc.Watch(args) 98 return parseErr(err, "watch") 99 } 100 101 // address a new address and displays it 102 if cmd == "adr" { 103 err = lc.Address(args) 104 return parseErr(err, "adr") 105 } 106 107 // ls shows the current set of utxos, addresses and score 108 if cmd == "ls" { 109 err = lc.Ls(args) 110 return parseErr(err, "ls") 111 } 112 113 // send sends coins to the address specified 114 if cmd == "send" { 115 err = lc.Send(args) 116 return parseErr(err, "send") 117 } 118 119 if cmd == "lis" { // listen for lnd peers 120 err = lc.Lis(args) 121 return parseErr(err, "lis") 122 } 123 124 if cmd == "off" { // stop remote node 125 // actually returns an error 126 return lc.Stop(args) 127 } 128 129 if cmd == "sweep" { // make lots of 1-in 1-out txs 130 err = lc.Sweep(args) 131 return parseErr(err, "sweep") 132 } 133 134 // push money in a channel away from you 135 if cmd == "push" { 136 err = lc.Push(args) 137 return parseErr(err, "push") 138 } 139 140 if cmd == "add" { 141 err = lc.AddHTLC(args) 142 return parseErr(err, "add") 143 } 144 145 if cmd == "clear" { 146 err = lc.ClearHTLC(args) 147 return parseErr(err, "clear") 148 } 149 150 if cmd == "claim" { 151 err = lc.ClaimHTLC(args) 152 return parseErr(err, "claim") 153 } 154 155 if cmd == "con" { // connect to lnd host 156 err = lc.Connect(args) 157 return parseErr(err, "con") 158 } 159 160 if cmd == "dlc" { // the root command for Discreet log contracts 161 err = lc.Dlc(args) 162 return parseErr(err, "dlc") 163 } 164 165 // remote control 166 if cmd == "rcauth" { 167 err = lc.RemoteControlAuth(args) 168 return parseErr(err, "rcauth") 169 } 170 171 if cmd == "rcreq" { 172 err = lc.RemoteControlRequest(args) 173 return parseErr(err, "rcreq") 174 } 175 176 // fund and create a new channel 177 if cmd == "fund" { 178 err = lc.FundChannel(args) 179 return parseErr(err, "fund") 180 } 181 182 // mutually fund and create a new channel 183 if cmd == "dualfund" { 184 if (len(args) > 0 && args[0] == "-h") || len(args) == 0 { 185 err = lc.DualFund(args) 186 } else { 187 if args[0] == "start" { 188 err = lc.DualFundChannel(args[1:]) 189 } else if args[0] == "decline" { 190 err = lc.DualFundDecline(args[1:]) 191 } else if args[0] == "accept" { 192 err = lc.DualFundAccept(args[1:]) 193 } else { 194 fmt.Fprintf(color.Output, "dualfund error - unrecognized subcommand %s\n", args[0]) 195 } 196 } 197 198 if err != nil { 199 fmt.Fprintf(color.Output, "dualfund error: %s\n", err) 200 } 201 return nil 202 } 203 // cooperative close of a channel 204 if cmd == "close" { 205 err = lc.CloseChannel(args) 206 return parseErr(err, "close") 207 } 208 if cmd == "break" { 209 err = lc.BreakChannel(args) 210 return parseErr(err, "break") 211 } 212 if cmd == "say" { 213 err = lc.Say(args) 214 return parseErr(err, "say") 215 } 216 217 if cmd == "fan" { // fan-out tx 218 err = lc.Fan(args) 219 return parseErr(err, "fan") 220 } 221 if cmd == "fee" { // get fee rate for a wallet 222 err = lc.Fee(args) 223 return parseErr(err, "fee") 224 } 225 if cmd == "dump" { // dump all private keys 226 err = lc.Dump(args) 227 return parseErr(err, "dump") 228 } 229 if cmd == "history" { // dump justice tx history 230 err = lc.History(args) 231 return parseErr(err, "history") 232 } 233 if cmd == "graph" { // dump graphviz for channels 234 err = lc.Graph(args) 235 return parseErr(err, "grpah") 236 } 237 if cmd == "paymultihop" { // pay via multi-hop 238 err = lc.PayMultihop(args) 239 if err != nil { 240 fmt.Fprintf(color.Output, "paymultihop error: %s\n", err) 241 } 242 return nil 243 } 244 245 fmt.Fprintf(color.Output, "Command not recognized. type help for command list.\n") 246 return nil 247 } 248 249 func (lc *litAfClient) Exit(textArgs []string) error { 250 if len(textArgs) > 0 { 251 if len(textArgs) == 1 && textArgs[0] == "-h" { 252 fmt.Fprintf(color.Output, exitCommand.Format) 253 fmt.Fprintf(color.Output, exitCommand.Description) 254 return nil 255 } 256 fmt.Fprintf(color.Output, "Unexpected argument: "+textArgs[0]) 257 return nil 258 } 259 return fmt.Errorf("User exit") 260 } 261 262 func (lc *litAfClient) Ls2(textArgs []string) error { 263 resp, err := http.Post("http://127.0.0.1:9750/lit", 264 "application/json", 265 bytes.NewBufferString( 266 `{"jsonrpc":"2.0","id":1,"method":"LitRPC.TxoList","params":[]}`)) 267 if err != nil { 268 return err 269 } 270 defer resp.Body.Close() 271 b, err := ioutil.ReadAll(resp.Body) 272 if err != nil { 273 return err 274 } 275 fmt.Printf("JSON over HTTP response: %s\n", string(b)) 276 return nil 277 } 278 279 func isExists(array []string, elem string) bool { 280 for _, x := range array { 281 if x == elem { 282 return true 283 } 284 } 285 return false 286 } 287 288 func (lc *litAfClient) Ls(textArgs []string) error { 289 stopEx, err := CheckHelpCommand(lsCommand, textArgs, 1) 290 if err != nil || stopEx { 291 return err 292 } 293 294 listofCommands := []string{"conns", "chans", "dualfunds", "txos", "ports", "addrs", "bals", "pays", "-a"} 295 cmd := textArgs[0] 296 297 if !isExists(listofCommands, cmd) { 298 return fmt.Errorf("Invalid Argument passed. Use ls -h for help") 299 } 300 301 if len(textArgs) > 1 { 302 return fmt.Errorf("Only provide one argument to ls. Use ls -h for help") 303 } 304 305 // TODO Move these to their respective places? Perhaps this gets optimized out anyways. 306 pReply := new(litrpc.ListConnectionsReply) 307 cReply := new(litrpc.ChannelListReply) 308 aReply := new(litrpc.AddressReply) 309 tReply := new(litrpc.TxoListReply) 310 bReply := new(litrpc.BalanceReply) 311 lReply := new(litrpc.ListeningPortsReply) 312 rcReply := new(litrpc.RCPendingAuthRequestsReply) 313 dfReply := new(litrpc.PendingDualFundReply) 314 mhReply := new(litrpc.MultihopPaymentsReply) 315 316 displayAllCommands := false 317 if cmd == "-a" { 318 displayAllCommands = true 319 } 320 321 // Balance reply needed for bals and chans 322 if cmd == "chans" || cmd == "bals" || displayAllCommands { 323 err := lc.Call("LitRPC.Balance", nil, bReply) 324 if err != nil { 325 return err 326 } 327 } 328 329 if cmd == "conns" || displayAllCommands { 330 err := lc.Call("LitRPC.ListConnections", nil, pReply) 331 if err != nil { 332 return err 333 } 334 335 if len(pReply.Connections) > 0 { 336 if displayAllCommands { 337 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Peers:")) 338 } 339 for _, peer := range pReply.Connections { 340 fmt.Fprintf(color.Output, "%s %s (%s)\n", 341 lnutil.White(peer.PeerNumber), peer.RemoteHost, peer.LitAdr) 342 } 343 } 344 } 345 346 err = lc.Call("LitRPC.Balance", nil, bReply) 347 if err != nil { 348 return err 349 } 350 351 walHeights := map[uint32]int32{} 352 for _, b := range bReply.Balances { 353 walHeights[b.CoinType] = b.SyncHeight 354 } 355 356 err = lc.Call("LitRPC.ChannelList", nil, cReply) 357 if err != nil { 358 return err 359 } 360 if len(cReply.Channels) > 0 { 361 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Channels:")) 362 } 363 364 if cmd == "chans" || displayAllCommands { 365 err := lc.Call("LitRPC.ChannelList", nil, cReply) 366 if err != nil { 367 return err 368 } 369 370 if len(cReply.Channels) > 0 { 371 if displayAllCommands { 372 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Channels:")) 373 } 374 375 coinDaemonConnected := map[uint32]bool{} 376 for _, walBal := range bReply.Balances { 377 coinDaemonConnected[walBal.CoinType] = true 378 } 379 380 sort.Slice(cReply.Channels, func(i, j int) bool { 381 return cReply.Channels[i].Height < cReply.Channels[j].Height 382 }) 383 384 var closedChannels []litrpc.ChannelInfo 385 var openChannels []litrpc.ChannelInfo 386 var disabledChannels []litrpc.ChannelInfo 387 var unconfirmedChannels []litrpc.ChannelInfo 388 for _, c := range cReply.Channels { 389 _, ok := coinDaemonConnected[c.CoinType] 390 if !ok { 391 disabledChannels = append(disabledChannels, c) 392 } else if c.Closed { 393 closedChannels = append(closedChannels, c) 394 } else if c.Height == -1 { 395 unconfirmedChannels = append(unconfirmedChannels, c) 396 } else { 397 openChannels = append(openChannels, c) 398 } 399 } 400 401 printChannel := func(c litrpc.ChannelInfo) { 402 fmt.Fprintf( 403 color.Output, 404 "\t\t%s (peer %d) type %d cap: %s bal: %s\n\t\t\t%s\n\t\t\th: %d state: %d\n\t\t\tdata: %x\n\t\t\tpkh: %x\n", 405 lnutil.White(c.CIdx), c.PeerIdx, c.CoinType, lnutil.SatoshiColor(c.Capacity), lnutil.SatoshiColor(c.MyBalance), 406 lnutil.OutPoint(c.OutPoint), 407 c.Height, c.StateNum, c.Data, c.Pkh) 408 409 var nHTLCs int 410 for _, h := range c.HTLCs { 411 if !h.Cleared && !h.ClearedOnChain { 412 nHTLCs++ 413 } 414 } 415 416 if len(c.HTLCs) > 0 { 417 fmt.Fprintf(color.Output, "\t\t\t%s\n", lnutil.Header("HTLCs:")) 418 } 419 420 for _, h := range c.HTLCs { 421 walHeight := walHeights[c.CoinType] 422 locktime := int32(h.Locktime) - walHeight 423 424 if h.Cleared || h.ClearedOnChain { 425 // Don't bother showing cleared HTLCs that are > 2* the default lock time old 426 if locktime < -2*consts.DefaultLockTime { 427 continue 428 } 429 430 fmt.Fprintf(color.Output, lnutil.Red("\tCleared: ")) 431 } else if h.Clearing || h.InProg { 432 c := color.New(color.FgYellow) 433 c.Printf("\t\t\t\tIn progress: ") 434 } else { 435 fmt.Fprintf(color.Output, lnutil.Green("\tUncleared: ")) 436 } 437 438 fmt.Fprintf(color.Output, 439 "%s incoming: %t amt: %s RHash: %x R: %x locktime: %d\n", 440 lnutil.White(h.Idx), h.Incoming, lnutil.SatoshiColor(h.Amt), h.RHash, 441 h.R, 442 locktime) 443 } 444 } 445 446 printChannels := func(cs []litrpc.ChannelInfo, title string) { 447 if len(cs) > 0 { 448 fmt.Fprintf(color.Output, "\t\t%s\n", title) 449 for _, c := range cs { 450 printChannel(c) 451 } 452 } 453 } 454 455 printChannels(openChannels, lnutil.Green("Open:")) 456 printChannels(unconfirmedChannels, lnutil.Yellow("Unconfirmed:")) 457 printChannels(closedChannels, lnutil.Red("Closed:")) 458 printChannels(disabledChannels, lnutil.Red("Disabled (coin daemon unavailable):")) 459 } 460 } 461 462 if cmd == "dualfunds" || displayAllCommands { 463 err := lc.Call("LitRPC.PendingDualFund", nil, dfReply) 464 if err != nil { 465 return err 466 } 467 468 if dfReply.Pending { 469 if displayAllCommands { 470 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Pending Dualfunds:")) 471 } 472 fmt.Fprintf( 473 color.Output, "\t%s %d\t%s %d\t%s %s\t%s %s\n\n", 474 lnutil.Header("Peer:"), dfReply.PeerIdx, 475 lnutil.Header("Type:"), dfReply.CoinType, 476 lnutil.Header("Their Amt:"), lnutil.SatoshiColor(dfReply.TheirAmount), 477 lnutil.Header("Req Amt:"), lnutil.SatoshiColor(dfReply.RequestedAmount), 478 ) 479 } 480 } 481 482 if cmd == "txos" || displayAllCommands { 483 err := lc.Call("LitRPC.TxoList", nil, tReply) 484 485 if err != nil { 486 return err 487 } 488 489 if len(tReply.Txos) > 0 { 490 if displayAllCommands { 491 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Txos:")) 492 } 493 for i, t := range tReply.Txos { 494 fmt.Fprintf(color.Output, "%d %s h:%d amt:%s %s %s", 495 i+1, lnutil.OutPoint(t.OutPoint), t.Height, 496 lnutil.SatoshiColor(t.Amt), t.KeyPath, t.CoinType) 497 if t.Delay != 0 { 498 fmt.Fprintf(color.Output, " delay: %d", t.Delay) 499 } 500 if !t.Witty { 501 fmt.Fprintf(color.Output, " non-witness") 502 } 503 fmt.Fprintf(color.Output, "\n") 504 } 505 } 506 } 507 508 if cmd == "rcauth" || displayAllCommands { 509 err := lc.Call("LitRPC.ListPendingRemoteControlAuthRequests", nil, rcReply) 510 if err != nil { 511 return err 512 } 513 if len(rcReply.PubKeys) > 0 { 514 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Nodes requesting remote control authorization:")) 515 for _, pubKey := range rcReply.PubKeys { 516 fmt.Fprintf(color.Output, "%x\n", pubKey) 517 } 518 } 519 } 520 521 if cmd == "ports" || displayAllCommands { 522 err := lc.Call("LitRPC.GetListeningPorts", nil, lReply) 523 if err != nil { 524 return err 525 } 526 527 if len(lReply.LisIpPorts) > 0 { 528 if displayAllCommands { 529 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Listening Ports:")) 530 } 531 fmt.Fprintf(color.Output, 532 "Listening for connections on port(s) %v with key %s\n", 533 lnutil.White(lReply.LisIpPorts), lReply.Adr) 534 } 535 } 536 537 if cmd == "addrs" || displayAllCommands { 538 err := lc.Call("LitRPC.Address", nil, aReply) 539 if err != nil { 540 return err 541 } 542 543 if len(aReply.WitAddresses) > 0 { 544 if displayAllCommands { 545 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Addresses:")) 546 } 547 for i, a := range aReply.WitAddresses { 548 fmt.Fprintf(color.Output, "%d %s (%s)\n", i+1, 549 lnutil.Address(a), lnutil.Address(aReply.LegacyAddresses[i])) 550 } 551 } 552 553 } 554 555 if cmd == "bals" || displayAllCommands { 556 if len(bReply.Balances) > 0 { 557 if displayAllCommands { 558 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Balances:")) 559 } 560 for _, walBal := range bReply.Balances { 561 fmt.Fprintf( 562 color.Output, "%s %d\t%s %d\t%s %d\t%s %s\t%s %s %s %s\n", 563 lnutil.Header("Type:"), walBal.CoinType, 564 lnutil.Header("Sync Height:"), walBal.SyncHeight, 565 lnutil.Header("FeeRate:"), walBal.FeeRate, 566 lnutil.Header("Utxo:"), lnutil.SatoshiColor(walBal.TxoTotal), 567 lnutil.Header("WitConf:"), lnutil.SatoshiColor(walBal.MatureWitty), 568 lnutil.Header("Channel:"), lnutil.SatoshiColor(walBal.ChanTotal), 569 ) 570 } 571 } 572 } 573 574 if cmd == "pays" || displayAllCommands { 575 err = lc.Call("LitRPC.ListMultihopPayments", nil, mhReply) 576 if err != nil { 577 return err 578 } 579 580 if len(mhReply.Payments) > 0 { 581 fmt.Fprintf(color.Output, "\t%s\n", lnutil.Header("Multihop Payments:")) 582 } 583 584 for _, p := range mhReply.Payments { 585 if p.Succeeded { 586 fmt.Fprintf(color.Output, lnutil.Green("Completed: ")) 587 } else { 588 c := color.New(color.FgYellow) 589 c.Printf("Pending: ") 590 } 591 592 path := p.Path[0] 593 for i := 1; i < len(p.Path); i++ { 594 path += " -> " + p.Path[i] 595 } 596 597 fmt.Fprintf(color.Output, 598 "amt: %s RHash: %x R: %x path: %s \n", 599 lnutil.SatoshiColor(p.Amt), p.RHash, 600 p.R, 601 path) 602 } 603 } 604 605 return nil 606 } 607 608 func (lc *litAfClient) Stop(textArgs []string) error { 609 if len(textArgs) > 0 && textArgs[0] == "-h" { 610 fmt.Fprintf(color.Output, offCommand.Format) 611 fmt.Fprintf(color.Output, offCommand.Description) 612 return nil 613 } 614 615 reply := new(litrpc.StatusReply) 616 617 err := lc.Call("LitRPC.Stop", nil, reply) 618 if err != nil { 619 return err 620 } 621 622 fmt.Fprintf(color.Output, "%s\n", reply.Status) 623 624 //lc.rpccon.Close() 625 return fmt.Errorf("stopped remote lit node") 626 } 627 628 func printHelp(commands []*Command) { 629 for _, command := range commands { 630 fmt.Fprintf(color.Output, "%s\t%s", command.Format, command.ShortDescription) 631 } 632 } 633 634 func printCointypes() { 635 for k, v := range coinparam.RegisteredNets { 636 fmt.Fprintf(color.Output, "CoinType: %s\n", strconv.Itoa(int(k))) 637 fmt.Fprintf(color.Output, "└────── Name: %-13sBech32Prefix: %s\n\n", v.Name+",", v.Bech32Prefix) 638 } 639 } 640 641 func (lc *litAfClient) Help(textArgs []string) error { 642 if len(textArgs) == 0 { 643 644 fmt.Fprintf(color.Output, lnutil.Header("Commands:\n")) 645 listofCommands := []*Command{helpCommand, sayCommand, lsCommand, addressCommand, sendCommand, fanCommand, sweepCommand, lisCommand, conCommand, dlcCommand, fundCommand, dualFundCommand, watchCommand, pushCommand, closeCommand, breakCommand, addHTLCCommand, clearHTLCCommand, rcAuthCommand, rcRequestCommand, historyCommand, offCommand, exitCommand} 646 printHelp(listofCommands) 647 fmt.Fprintf(color.Output, "\n\n") 648 fmt.Fprintf(color.Output, lnutil.Header("Coins:\n")) 649 printCointypes() 650 651 return nil 652 } 653 654 if textArgs[0] == "help" || textArgs[0] == "-h" { 655 fmt.Fprintf(color.Output, helpCommand.Format) 656 fmt.Fprintf(color.Output, helpCommand.Description) 657 return nil 658 } 659 res := make([]string, 0) 660 res = append(res, textArgs[0]) 661 res = append(res, "-h") 662 return lc.Shellparse(res) 663 }