package main import ( "bufio" "fmt" "log" "os" "sort" "strconv" "strings" "github.com/roasbeef/btcd/chaincfg" "github.com/roasbeef/btcutil" "github.com/lightningnetwork/lnd/uspv" ) /* this is a CLI shell for testing out LND. Right now it's only for uspv testing. It can send and receive coins. */ const ( keyFileName = "testkey.hex" headerFileName = "headers.bin" dbFileName = "utxo.db" // this is my local testnet node, replace it with your own close by. // Random internet testnet nodes usually work but sometimes don't, so // maybe I should test against different versions out there. SPVHostAdr = "127.0.0.1:28333" ) var ( Params = &chaincfg.SegNet4Params SCon uspv.SPVCon // global here for now ) func shell(SPVHostAdr string, Params *chaincfg.Params) { fmt.Printf("LND spv shell v0.0\n") fmt.Printf("Not yet well integrated, but soon.\n") // read key file (generate if not found) rootPriv, err := uspv.ReadKeyFileToECPriv(keyFileName, Params) if err != nil { log.Fatal(err) } // setup TxStore first (before spvcon) Store := uspv.NewTxStore(rootPriv, Params) // setup spvCon SCon, err = uspv.OpenSPV( SPVHostAdr, headerFileName, dbFileName, &Store, true, false, Params) if err != nil { log.Fatal(err) } tip, err := SCon.TS.GetDBSyncHeight() // ask for sync height if err != nil { log.Fatal(err) } if tip == 0 { // DB has never been used, set to birthday tip = 21900 // hardcoded; later base on keyfile date? err = SCon.TS.SetDBSyncHeight(tip) if err != nil { log.Fatal(err) } } // once we're connected, initiate headers sync err = SCon.AskForHeaders() if err != nil { log.Fatal(err) } // main shell loop for { // setup reader with max 4K input chars reader := bufio.NewReaderSize(os.Stdin, 4000) fmt.Printf("LND# ") // prompt msg, err := reader.ReadString('\n') // input finishes on enter key if err != nil { log.Fatal(err) } cmdslice := strings.Fields(msg) // chop input up on whitespace if len(cmdslice) < 1 { continue // no input, just prompt again } fmt.Printf("entered command: %s\n", msg) // immediate feedback err = Shellparse(cmdslice) if err != nil { // only error should be user exit log.Fatal(err) } } return } // Shellparse parses user input and hands it to command functions if matching func Shellparse(cmdslice []string) error { var err error var args []string cmd := cmdslice[0] if len(cmdslice) > 1 { args = cmdslice[1:] } if cmd == "exit" || cmd == "quit" { return fmt.Errorf("User exit") } // help gives you really terse help. Just a list of commands. if cmd == "help" { err = Help(args) if err != nil { fmt.Printf("help error: %s\n", err) } return nil } // adr generates a new address and displays it if cmd == "adr" { err = Adr(args) if err != nil { fmt.Printf("adr error: %s\n", err) } return nil } // bal shows the current set of utxos, addresses and score if cmd == "bal" { err = Bal(args) if err != nil { fmt.Printf("bal error: %s\n", err) } return nil } // send sends coins to the address specified if cmd == "send" { err = Send(args) if err != nil { fmt.Printf("send error: %s\n", err) } return nil } if cmd == "fan" { err = Fan(args) if err != nil { fmt.Printf("fan error: %s\n", err) } return nil } if cmd == "sweep" { err = Sweep(args) if err != nil { fmt.Printf("sweep error: %s\n", err) } return nil } if cmd == "txs" { err = Txs(args) if err != nil { fmt.Printf("txs error: %s\n", err) } return nil } if cmd == "blk" { err = Blk(args) if err != nil { fmt.Printf("blk error: %s\n", err) } return nil } fmt.Printf("Command not recognized. type help for command list.\n") return nil } func Txs(args []string) error { alltx, err := SCon.TS.GetAllTxs() if err != nil { return err } for i, tx := range alltx { fmt.Printf("tx %d %s\n", i, uspv.TxToString(tx)) } return nil } func Blk(args []string) error { if SCon.RBytes == 0 { return fmt.Errorf("Can't check block, spv connection broken") } if len(args) == 0 { return fmt.Errorf("must specify height") } height, err := strconv.ParseInt(args[0], 10, 32) if err != nil { return err } // request most recent block just to test err = SCon.AskForOneBlock(int32(height)) if err != nil { return err } return nil } // Bal prints out your score. func Bal(args []string) error { if SCon.TS == nil { return fmt.Errorf("Can't get balance, spv connection broken") } fmt.Printf(" ----- Account Balance ----- \n") rawUtxos, err := SCon.TS.GetAllUtxos() if err != nil { return err } var allUtxos uspv.SortableUtxoSlice for _, utxo := range rawUtxos { allUtxos = append(allUtxos, *utxo) } // smallest and unconfirmed last (because it's reversed) sort.Sort(sort.Reverse(allUtxos)) var score, confScore int64 for i, u := range allUtxos { fmt.Printf("\tutxo %d height %d %s key:%d amt %d", i, u.AtHeight, u.Op.String(), u.KeyIdx, u.Value) if u.IsWit { fmt.Printf(" WIT") } fmt.Printf("\n") score += u.Value if u.AtHeight != 0 { confScore += u.Value } } height, _ := SCon.TS.GetDBSyncHeight() atx, err := SCon.TS.GetAllTxs() stxos, err := SCon.TS.GetAllStxos() for i, a := range SCon.TS.Adrs { wa, err := btcutil.NewAddressWitnessPubKeyHash( a.PkhAdr.ScriptAddress(), Params) if err != nil { return err } fmt.Printf("address %d %s OR %s\n", i, a.PkhAdr.String(), wa.String()) } fmt.Printf("Total known txs: %d\n", len(atx)) fmt.Printf("Known utxos: %d\tPreviously spent txos: %d\n", len(allUtxos), len(stxos)) fmt.Printf("Total coin: %d confirmed: %d\n", score, confScore) fmt.Printf("DB sync height: %d\n", height) return nil } // Adr makes a new address. func Adr(args []string) error { // if there's an arg, make 10 adrs if len(args) > 0 { for i := 0; i < 10; i++ { _, err := SCon.TS.NewAdr() if err != nil { return err } } } if len(args) > 1 { for i := 0; i < 1000; i++ { _, err := SCon.TS.NewAdr() if err != nil { return err } } } // always make one a, err := SCon.TS.NewAdr() if err != nil { return err } fmt.Printf("made new address %s\n", a.String()) return nil } // Sweep sends every confirmed uxto in your wallet to an address. // it does them all individually to there are a lot of txs generated. // syntax: sweep adr func Sweep(args []string) error { if len(args) < 2 { return fmt.Errorf("sweep syntax: sweep adr") } adr, err := btcutil.DecodeAddress(args[0], SCon.TS.Param) if err != nil { fmt.Printf("error parsing %s as address\t", args[0]) return err } numTxs, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return err } if numTxs < 1 { return fmt.Errorf("can't send %d txs", numTxs) } rawUtxos, err := SCon.TS.GetAllUtxos() if err != nil { return err } var allUtxos uspv.SortableUtxoSlice for _, utxo := range rawUtxos { allUtxos = append(allUtxos, *utxo) } // smallest and unconfirmed last (because it's reversed) sort.Sort(sort.Reverse(allUtxos)) for i, u := range allUtxos { if u.AtHeight != 0 { err = SCon.SendOne(allUtxos[i], adr) if err != nil { return err } numTxs-- if numTxs == 0 { return nil } } } fmt.Printf("spent all confirmed utxos; not enough by %d\n", numTxs) return nil } // Fan generates a bunch of fanout. Only for testing, can be expensive. // syntax: fan adr numOutputs valOutputs witty func Fan(args []string) error { if len(args) < 3 { return fmt.Errorf("fan syntax: fan adr numOutputs valOutputs") } adr, err := btcutil.DecodeAddress(args[0], SCon.TS.Param) if err != nil { fmt.Printf("error parsing %s as address\t", args[0]) return err } numOutputs, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return err } valOutputs, err := strconv.ParseInt(args[2], 10, 64) if err != nil { return err } adrs := make([]btcutil.Address, numOutputs) amts := make([]int64, numOutputs) for i := int64(0); i < numOutputs; i++ { adrs[i] = adr amts[i] = valOutputs + i } return SCon.SendCoins(adrs, amts) } // Send sends coins. func Send(args []string) error { if SCon.RBytes == 0 { return fmt.Errorf("Can't send, spv connection broken") } // get all utxos from the database allUtxos, err := SCon.TS.GetAllUtxos() if err != nil { return err } var score int64 // score is the sum of all utxo amounts. highest score wins. // add all the utxos up to get the score for _, u := range allUtxos { score += u.Value } // score is 0, cannot unlock 'send coins' acheivement if score == 0 { return fmt.Errorf("You don't have money. Work hard.") } // need args, fail if len(args) < 2 { return fmt.Errorf("need args: ssend address amount(satoshis) wit?") } adr, err := btcutil.DecodeAddress(args[0], SCon.TS.Param) if err != nil { fmt.Printf("error parsing %s as address\t", args[0]) return err } amt, err := strconv.ParseInt(args[1], 10, 64) if err != nil { return err } if amt < 1000 { return fmt.Errorf("can't send %d, too small", amt) } fmt.Printf("send %d to address: %s \n", amt, adr.String()) var adrs []btcutil.Address var amts []int64 adrs = append(adrs, adr) amts = append(amts, amt) err = SCon.SendCoins(adrs, amts) if err != nil { return err } return nil } func Help(args []string) error { fmt.Printf("commands:\n") fmt.Printf("help adr bal send exit\n") return nil }