-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): get balance for an account (#109)
* feat(cli): get the balance of an account in wei - accept block height as quantity of tag (only "latest" supported for now) - allow conversion of the output from wei to ether - accept any compatible rpc endpoint * test(cli): initial test suite for balance command * docs(readme): updated with balance command * refactor(cli): factory pattern * refactor(cli): const flags and cobra stdout support * fix(cli): fix output redirection and little refactoring
- Loading branch information
Showing
3 changed files
with
211 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
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/params" | ||
) | ||
|
||
const ( | ||
flagBalanceBlock = "block" | ||
flagBalanceEther = "ether" | ||
flagBalanceRpcUrl = "rpc-url" | ||
) | ||
|
||
func init() { | ||
rootCmd.AddCommand(NewBalanceCmd()) | ||
} | ||
|
||
func NewBalanceCmd() *cobra.Command { | ||
c := &balance{} | ||
cmd := &cobra.Command{ | ||
Use: "balance [account]", | ||
Short: "Get the balance of an account", | ||
Aliases: []string{"b"}, | ||
Args: cobra.ExactArgs(1), | ||
RunE: c.Run, | ||
} | ||
|
||
cmd.Flags().StringP(flagBalanceBlock, "B", "latest", "The block height to query at") | ||
cmd.Flags().BoolP(flagBalanceEther, "e", false, "Format the balance in ether") | ||
cmd.Flags().StringP(flagBalanceRpcUrl, "r", "", "The RPC endpoint to the blockchain node to interact with") | ||
|
||
return cmd | ||
} | ||
|
||
type balance struct { | ||
} | ||
|
||
func (c *balance) Run(cmd *cobra.Command, args []string) error { | ||
fAccount := cmd.Flags().Args()[0] | ||
fBlock, err := cmd.Flags().GetString(flagBalanceBlock) | ||
if err != nil { | ||
return err | ||
} | ||
fEther, err := cmd.Flags().GetBool(flagBalanceEther) | ||
if err != nil { | ||
return err | ||
} | ||
fRpc, err := cmd.Flags().GetString(flagBalanceRpcUrl) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !common.IsHexAddress(fAccount) { | ||
return errors.New("error: please provide a valid account address (e.g. 0x213a286A1AF3Ac010d4F2D66A52DeAf762dF7742)") | ||
} | ||
|
||
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 | ||
} | ||
|
||
block, err := strconv.ParseUint(fBlock, 10, 64) | ||
if err != nil { | ||
// TODO: implement support for all tags: earliest, latest, pending, finalized, safe | ||
if fBlock == "latest" { | ||
bh, err := provider.BlockNumber(context.Background()) | ||
if err != nil { | ||
return err | ||
} | ||
block = bh | ||
} else { | ||
return errors.New("error: invalid block height") | ||
} | ||
} | ||
|
||
wei, err := provider.BalanceAt(context.Background(), common.HexToAddress(fAccount), big.NewInt(int64(block))) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if fEther { | ||
bal := weiToEther(wei) | ||
fmt.Fprintln(cmd.OutOrStdout(), bal, "ether") | ||
} else { | ||
fmt.Fprintln(cmd.OutOrStdout(), wei, "wei") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// https://github.com/ethereum/go-ethereum/issues/21221 | ||
func weiToEther(wei *big.Int) *big.Float { | ||
f := new(big.Float) | ||
f.SetPrec(236) // IEEE 754 octuple-precision binary floating-point format: binary256 | ||
f.SetMode(big.ToNearestEven) | ||
fWei := new(big.Float) | ||
fWei.SetPrec(236) // IEEE 754 octuple-precision binary floating-point format: binary256 | ||
fWei.SetMode(big.ToNearestEven) | ||
|
||
return f.Quo(fWei.SetInt(wei), big.NewFloat(params.Ether)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"math" | ||
"strconv" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func execute(args string) (string, error) { | ||
cmd := NewBalanceCmd() | ||
actual := new(bytes.Buffer) | ||
cmd.SetOut(actual) | ||
cmd.SetErr(actual) | ||
cmd.SetArgs(strings.Split(args, " ")) | ||
if err := cmd.Execute(); err != nil { | ||
return "", err | ||
} | ||
|
||
return actual.String(), nil | ||
} | ||
|
||
func Test_BalanceCmd(t *testing.T) { | ||
res, err := execute("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") | ||
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") | ||
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") | ||
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") | ||
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)) | ||
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") | ||
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") | ||
assert.NotNil(t, err) | ||
assert.Empty(t, res) | ||
} |