Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): get block by number #112

Merged
merged 8 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading