Skip to content

Commit

Permalink
feat(cli): get block by number (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
czar0 authored Jan 10, 2024
1 parent 04e7f83 commit 3f76064
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 20 deletions.
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ Ethkit comes equipped with the `ethkit` CLI providing:
with scrypt wallet encryption support.
- **Abigen** - generate Go code from an ABI artifact file to interact with or deploy a smart
contract.
- **Artifacts** - parse details from a Truffle artifact file from the command line such as contract
bytecode or the JSON abi.
- **Balance** - retrieve the balance of an account at any block height for any supported network via RPC.
- **Artifacts** - parse details from a Truffle artifact file from command line such as contract
bytecode or the json abi
- **Balance** - retrieve the balance of an account at any block height for any supported network via RPC
- **Block** - retrieve the block information based on block height (or tag) and filtered by optional input parameters

## Install

Expand Down Expand Up @@ -107,6 +108,24 @@ Flags:
-r, --rpc-url string The RPC endpoint to the blockchain node to interact with
```

### block

`block` retrieves a block by a provided block height or tag via RPC

```bash
Usage:
ethkit block [number|tag] [flags]

Aliases:
block, bl

Flags:
-f, --field string Get the specific field of a block
--full Get the full block information
-h, --help help for block
-j, --json Print the block as JSON
```

## Ethkit Go Development Library

Ethkit is a very capable Ethereum development library for writing systems in Go that
Expand Down
18 changes: 9 additions & 9 deletions cmd/ethkit/balance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/assert"
)

func execute(args string) (string, error) {
func execBalanceCmd(args string) (string, error) {
cmd := NewBalanceCmd()
actual := new(bytes.Buffer)
cmd.SetOut(actual)
Expand All @@ -25,53 +25,53 @@ func execute(args string) (string, error) {
}

func Test_BalanceCmd(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia")
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia")
assert.Nil(t, err)
assert.NotNil(t, res)
}

func Test_BalanceCmd_ValidWei(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia")
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia")
assert.Nil(t, err)
assert.Equal(t, res, fmt.Sprintln(strconv.Itoa(500_000_000_000_000_000), "wei"))
}

func Test_BalanceCmd_ValidEther(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --ether")
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --ether")
assert.Nil(t, err)
assert.Equal(t, res, fmt.Sprintln(strconv.FormatFloat(0.5, 'f', -1, 64), "ether"))
}

func Test_BalanceCmd_InvalidAddress(t *testing.T) {
res, err := execute("0x1 --rpc-url https://nodes.sequence.app/sepolia")
res, err := execBalanceCmd("0x1 --rpc-url https://nodes.sequence.app/sepolia")
assert.NotNil(t, err)
assert.Empty(t, res)
assert.Contains(t, err.Error(), "please provide a valid account address")
}

func Test_BalanceCmd_InvalidRPC(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url nodes.sequence.app/sepolia")
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url nodes.sequence.app/sepolia")
assert.NotNil(t, err)
assert.Empty(t, res)
assert.Contains(t, err.Error(), "please provide a valid rpc url")
}

func Test_BalanceCmd_NotExistingBlockHeigh(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --block " + fmt.Sprint(math.MaxInt64))
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --block " + fmt.Sprint(math.MaxInt64))
assert.NotNil(t, err)
assert.Empty(t, res)
assert.Contains(t, err.Error(), "jsonrpc error -32000: header not found")
}

func Test_BalanceCmd_NotAValidStringBlockHeigh(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --block something")
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --block something")
assert.NotNil(t, err)
assert.Empty(t, res)
assert.Contains(t, err.Error(), "invalid block height")
}

func Test_BalanceCmd_NotAValidNumberBlockHeigh(t *testing.T) {
res, err := execute("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --block -100")
res, err := execBalanceCmd("0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742 --rpc-url https://nodes.sequence.app/sepolia --block -100")
assert.NotNil(t, err)
assert.Empty(t, res)
}
256 changes: 256 additions & 0 deletions cmd/ethkit/block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package main

import (
"context"
"errors"
"fmt"
"math/big"
"net/url"
"strconv"

"github.com/spf13/cobra"

"github.com/0xsequence/ethkit/ethrpc"
"github.com/0xsequence/ethkit/go-ethereum/common"
"github.com/0xsequence/ethkit/go-ethereum/core/types"
)

const (
flagBlockField = "field"
flagBlockFull = "full"
flagBlockRpcUrl = "rpc-url"
flagBlockJson = "json"
)

func init() {
rootCmd.AddCommand(NewBlockCmd())
}

type block struct {
}

// NewBlockCommand returns a new build command to retrieve a block.
func NewBlockCmd() *cobra.Command {
c := &block{}
cmd := &cobra.Command{
Use: "block [number|tag]",
Short: "Get the information about the block",
Aliases: []string{"bl"},
Args: cobra.ExactArgs(1),
RunE: c.Run,
}

cmd.Flags().StringP(flagBlockField, "f", "", "Get the specific field of a block")
cmd.Flags().Bool(flagBlockFull, false, "Get the full block information")
cmd.Flags().StringP(flagBlockRpcUrl, "r", "", "The RPC endpoint to the blockchain node to interact with")
cmd.Flags().BoolP(flagBlockJson, "j", false, "Print the block as JSON")

return cmd
}

func (c *block) Run(cmd *cobra.Command, args []string) error {
fBlock := cmd.Flags().Args()[0]
fField, err := cmd.Flags().GetString(flagBlockField)
if err != nil {
return err
}
fFull, err := cmd.Flags().GetBool(flagBlockFull)
if err != nil {
return err
}
fRpc, err := cmd.Flags().GetString(flagBlockRpcUrl)
if err != nil {
return err
}
fJson, err := cmd.Flags().GetBool(flagBlockJson)
if err != nil {
return err
}

if _, err = url.ParseRequestURI(fRpc); err != nil {
return errors.New("error: please provide a valid rpc url (e.g. https://nodes.sequence.app/mainnet)")
}

provider, err := ethrpc.NewProvider(fRpc)
if err != nil {
return err
}

bh, err := strconv.ParseUint(fBlock, 10, 64)
if err != nil {
// TODO: implement support for all tags: earliest, latest, pending, finalized, safe
return errors.New("error: invalid block height")
}

block, err := provider.BlockByNumber(context.Background(), big.NewInt(int64(bh)))
if err != nil {
return err
}

var obj any
obj = NewHeader(block)

if fFull {
obj = NewBlock(block)
}

if fField != "" {
obj = GetValueByJSONTag(obj, fField)
}

if fJson {
json, err := PrettyJSON(obj)
if err != nil {
return err
}
obj = *json
}

fmt.Fprintln(cmd.OutOrStdout(), obj)

return nil
}

// Header is a customized block header for cli.
type Header struct {
ParentHash common.Hash `json:"parentHash"`
UncleHash common.Hash `json:"sha3Uncles"`
Coinbase common.Address `json:"miner"`
Hash common.Hash `json:"hash"`
Root common.Hash `json:"stateRoot"`
TxHash common.Hash `json:"transactionsRoot"`
ReceiptHash common.Hash `json:"receiptsRoot"`
Bloom types.Bloom `json:"logsBloom"`
Difficulty *big.Int `json:"difficulty"`
Number *big.Int `json:"number"`
GasLimit uint64 `json:"gasLimit"`
GasUsed uint64 `json:"gasUsed"`
Time uint64 `json:"timestamp"`
Extra []byte `json:"extraData"`
MixDigest common.Hash `json:"mixHash"`
Nonce types.BlockNonce `json:"nonce"`
BaseFee *big.Int `json:"baseFeePerGas"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot"`
Size common.StorageSize `json:"size"`
// TODO: totalDifficulty to be implemented
// TotalDifficulty *big.Int `json:"totalDifficulty"`
TransactionsHash []common.Hash `json:"transactions"`
}

// NewHeader returns the custom-built Header object.
func NewHeader(b *types.Block) *Header {
return &Header{
ParentHash: b.Header().ParentHash,
UncleHash: b.Header().UncleHash,
Coinbase: b.Header().Coinbase,
Hash: b.Hash(),
Root: b.Header().Root,
TxHash: b.Header().TxHash,
ReceiptHash: b.ReceiptHash(),
Bloom: b.Bloom(),
Difficulty: b.Header().Difficulty,
Number: b.Header().Number,
GasLimit: b.Header().GasLimit,
GasUsed: b.Header().GasUsed,
Time: b.Header().Time,
Extra: b.Header().Extra,
MixDigest: b.Header().MixDigest,
Nonce: b.Header().Nonce,
BaseFee: b.Header().BaseFee,
WithdrawalsHash: b.Header().WithdrawalsHash,
Size: b.Size(),
// TotalDifficulty: b.Difficulty(),
TransactionsHash: TransactionsHash(*b),
}
}

// String overrides the standard behavior for Header "to-string".
func (h *Header) String() string {
var p Printable
if err := p.FromStruct(h); err != nil {
panic(err)
}
s := p.Columnize(*NewPrintableFormat(20, 0, 0, byte(' ')))

return s
}

// TransactionsHash returns a list of transaction hash starting from a list of transactions contained in a block.
func TransactionsHash(block types.Block) []common.Hash {
txsh := make([]common.Hash, len(block.Transactions()))

for i, tx := range block.Transactions() {
txsh[i] = tx.Hash()
}

return txsh
}

// Block is a customized block for cli.
type Block struct {
ParentHash common.Hash `json:"parentHash"`
UncleHash common.Hash `json:"sha3Uncles"`
Coinbase common.Address `json:"miner"`
Hash common.Hash `json:"hash"`
Root common.Hash `json:"stateRoot"`
TxHash common.Hash `json:"transactionsRoot"`
ReceiptHash common.Hash `json:"receiptsRoot"`
Bloom types.Bloom `json:"logsBloom"`
Difficulty *big.Int `json:"difficulty"`
Number *big.Int `json:"number"`
GasLimit uint64 `json:"gasLimit"`
GasUsed uint64 `json:"gasUsed"`
Time uint64 `json:"timestamp"`
Extra []byte `json:"extraData"`
MixDigest common.Hash `json:"mixHash"`
Nonce types.BlockNonce `json:"nonce"`
BaseFee *big.Int `json:"baseFeePerGas"`
WithdrawalsHash *common.Hash `json:"withdrawalsRoot"`
Size common.StorageSize `json:"size"`
// TODO: totalDifficulty to be implemented
// TotalDifficulty *big.Int `json:"totalDifficulty"`
Uncles []*types.Header `json:"uncles"`
Transactions types.Transactions `json:"transactions"`
Withdrawals types.Withdrawals `json:"withdrawals"`
}

// NewBlock returns the custom-built Block object.
func NewBlock(b *types.Block) *Block {
return &Block{
ParentHash: b.Header().ParentHash,
UncleHash: b.Header().UncleHash,
Coinbase: b.Header().Coinbase,
Hash: b.Hash(),
Root: b.Header().Root,
TxHash: b.Header().TxHash,
ReceiptHash: b.ReceiptHash(),
Bloom: b.Bloom(),
Difficulty: b.Header().Difficulty,
Number: b.Header().Number,
GasLimit: b.Header().GasLimit,
GasUsed: b.Header().GasUsed,
Time: b.Header().Time,
Extra: b.Header().Extra,
MixDigest: b.Header().MixDigest,
Nonce: b.Header().Nonce,
BaseFee: b.Header().BaseFee,
WithdrawalsHash: b.Header().WithdrawalsHash,
Size: b.Size(),
// TotalDifficulty: b.Difficulty(),
Uncles: b.Uncles(),
Transactions: b.Transactions(),
// TODO: Withdrawals is empty. To be fixed.
Withdrawals: b.Withdrawals(),
}
}

// String overrides the standard behavior for Block "to-string".
func (b *Block) String() string {
var p Printable
if err := p.FromStruct(b); err != nil {
panic(err)
}
s := p.Columnize(*NewPrintableFormat(20, 0, 0, byte(' ')))

return s
}
Loading

0 comments on commit 3f76064

Please sign in to comment.