Building a Blockchain in Go PT: III (extended) - CLI

nheindev

Noah Hein

Posted on April 4, 2021

Building a Blockchain in Go PT: III (extended) - CLI

Hey everyone! I hope you've stuck through with me this far. Things start to get a bit more complicated as we go along. I am trying to structure these in such a way that you don't need to stop between portions to learn other relevant material before coming back, but don't feel bad if you do have to do that.

I left off with a bit of a cliffhanger as I gave you an entirely new main.go file and just told you "Good luck!". This post will hopefully demystify that file for you if you didn't quite comprehend it. Maybe you took it upon yourself to see what was actually going on without my explaining of it. If you did, maybe you could comment below what the hardest part was for you?

Command Line

Alrighty, so we are looking to add cli capability to our app. If you have noticed a trend here, anytime I'm adding functionality, I start with a struct. This keeps things easier to reason about in my head.

//main.go
type CommandLine struct {
    blockchain *blockchain.BlockChain
}
Enter fullscreen mode Exit fullscreen mode

Notice that we have *blockchain.Blockchain. That is because we are no longer working within the blockchain package itself, so we have to call the package blockchain, and then the BlockChain struct within that package.

With that out of the way we can start adding methods to our new struct. I think a good place to start would be an introduction to the cli. Similar to a --help flag in many of your favorite packages or frameworks.

//main.go
//printUsage will display what options are availble to the user
func (cli *CommandLine) printUsage() {
    fmt.Println("Usage: ")
    fmt.Println(" add -block <BLOCK_DATA> - add a block to the chain")
    fmt.Println(" print - prints the blocks in the chain")
}
Enter fullscreen mode Exit fullscreen mode

Next we will want to close out our runtime if the user passes an invalid argument to our cli.

//main.go
//validateArgs ensures the cli was given valid input
func (cli *CommandLine) validateArgs() {
    if len(os.Args) < 2 {
        cli.printUsage()
        //go exit will exit the application by shutting down the goroutine
        // if you were to use os.exit you might corrupt the data
        runtime.Goexit()
    }
}
Enter fullscreen mode Exit fullscreen mode

Next we can use the AddBlock() method that we modified in our previous tutorial to......Add a block.

//main.go
//addBlock allows users to add blocks to the chain via the cli
func (cli *CommandLine) addBlock(data string) {
    cli.blockchain.AddBlock(data)
    fmt.Println("Added Block!")
}
Enter fullscreen mode Exit fullscreen mode

As a quick recap, we have a way to tell the user how the cli functions, we can add blocks to the chain, and users don't get stuck when providing invalid input. Next it would be nice if we could view our chain. Let's see what that looks like!

//main.go
//printChain will display the entire contents of the blockchain
func (cli *CommandLine) printChain() {
    iterator := cli.blockchain.Iterator()

    for {
        block := iterator.Next()
        fmt.Printf("Previous hash: %x\n", block.PrevHash)
        fmt.Printf("data: %s\n", block.Data)
        fmt.Printf("hash: %x\n", block.Hash)
        pow := blockchain.NewProofOfWork(block)
        fmt.Printf("Pow: %s\n", strconv.FormatBool(pow.Validate()))
        fmt.Println()
        // This works because the Genesis block has no PrevHash to point to.
        if len(block.PrevHash) == 0 {
            break
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we were able to use the Next() method that we coded in the previous post. This method is how we are able to navigate a for loop to print out our entire blockchain.

The last thing to do is code up a run() method. We will use a switch statement and the Parsed method that we get from the flag package.

//main.go
//run will start up the command line
func (cli *CommandLine) run() {
    cli.validateArgs()

    addBlockCmd := flag.NewFlagSet("add", flag.ExitOnError)
    printChainCmd := flag.NewFlagSet("print", flag.ExitOnError)
    addBlockData := addBlockCmd.String("block", "", "Block data")

    switch os.Args[1] {
    case "add":
        err := addBlockCmd.Parse(os.Args[2:])
        blockchain.Handle(err)

    case "print":
        err := printChainCmd.Parse(os.Args[2:])
        blockchain.Handle(err)

    default:
        cli.printUsage()
        runtime.Goexit()
    }
    // Parsed() will return true if the object it was used on has been called
    if addBlockCmd.Parsed() {
        if *addBlockData == "" {
            addBlockCmd.Usage()
            runtime.Goexit()
        }
        cli.addBlock(*addBlockData)
    }
    if printChainCmd.Parsed() {
        cli.printChain()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have all the tools in place to actually run a very barebones cli for our blockchain. The last thing we have to do is stick it in our main function.

//main.go
func main() {
    defer os.Exit(0)

    chain := blockchain.InitBlockChain()
    defer chain.Database.Close()

    cli := CommandLine{chain}

    cli.run()

}
Enter fullscreen mode Exit fullscreen mode

The defer keyword that we use is in place to make sure that we don't corrupt any of the bytes going into our database. It essentially just lets everything shutdown nicely. This is similar to why we use the Goexit() call instead of the similar OS exit that is also available. It allows time for all of our I/O to the database to finish up before exiting the program.

I hope you learned something from this. Maybe you already taught yourself all of this since I posted part III earlier! That's always where you learn the most, when you have to struggle a bit for that knowledge!

As always leave a comment if you have any questions or concerns. I'd love to help you out.

💖 💪 🙅 🚩
nheindev
Noah Hein

Posted on April 4, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related