diff --git a/cmd/hmyclient/main.go b/cmd/hmyclient/main.go new file mode 100644 index 000000000..c5fdfe350 --- /dev/null +++ b/cmd/hmyclient/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/harmony/hmyclient" +) + +// newRPCClient creates a rpc client with specified node URL. +func newRPCClient(url string) *rpc.Client { + client, err := rpc.Dial(url) + if err != nil { + fmt.Errorf("Failed to connect to Ethereum node: %v", err) + } + return client +} + +func main() { + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + rpcClient := newRPCClient("http://localhost:9500") + if rpcClient == nil { + fmt.Errorf("Failed to create rpc client") + } + client := hmyclient.NewClient(rpcClient) + if client == nil { + fmt.Errorf("Failed to create client") + } + + networkID, err := client.NetworkID(ctx) + if err != nil { + fmt.Errorf("Failed to get net_version: %v", err) + } + fmt.Printf("net_version: %v\n", networkID) + + blockNumber, err := client.BlockNumber(ctx) + if err != nil { + fmt.Errorf("Failed to get hmy_blockNumber: %v", err) + } + fmt.Printf("hmy_blockNumber: %v\n", blockNumber) + + block, err := client.BlockByNumber(ctx, new(big.Int).SetUint64(uint64(blockNumber))) + if err != nil { + fmt.Errorf("Failed to get hmy_getBlockByNumber %v: %v", blockNumber, err) + } + fmt.Printf("hmy_getBlockByNumber(%v): %v\n", blockNumber, block) + + block, err = client.BlockByNumber(ctx, nil) + if err != nil { + fmt.Errorf("Failed to get block: %v", err) + } + fmt.Printf("hmy_getBlockByNumber(latest): %v", block) +} diff --git a/core/types/block.go b/core/types/block.go index 581519799..3026b1c5d 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -69,7 +69,7 @@ func (n *BlockNonce) UnmarshalText(input []byte) error { return hexutil.UnmarshalFixedText("BlockNonce", input, n[:]) } -// Header represents a block header in the Ethereum blockchain. +// Header represents a block header in the Harmony blockchain. type Header struct { ParentHash common.Hash `json:"parentHash" gencodec:"required"` Coinbase common.Address `json:"miner" gencodec:"required"` diff --git a/hmyclient/hmyclient.go b/hmyclient/hmyclient.go new file mode 100644 index 000000000..4f54f437c --- /dev/null +++ b/hmyclient/hmyclient.go @@ -0,0 +1,157 @@ +package hmyclient + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/harmony/core/types" +) + +// NotFound is returned by API methods if the requested item does not exist. +var NotFound = errors.New("not found") + +// Client defines typed wrappers for the Ethereum RPC API. +type Client struct { + c *rpc.Client +} + +// Dial connects a client to the given URL. +func Dial(rawurl string) (*Client, error) { + return dialContext(context.Background(), rawurl) +} + +func dialContext(ctx context.Context, rawurl string) (*Client, error) { + c, err := rpc.DialContext(ctx, rawurl) + if err != nil { + return nil, err + } + return NewClient(c), nil +} + +// NewClient creates a client that uses the given RPC client. +func NewClient(c *rpc.Client) *Client { + return &Client{c} +} + +// Close closes the client +func (c *Client) Close() { + c.c.Close() +} + +// BlockNumber returns the block height. +func (c *Client) BlockNumber(ctx context.Context) (hexutil.Uint64, error) { + var raw json.RawMessage + err := c.c.CallContext(ctx, &raw, "hmy_blockNumber") + if err != nil { + return 0, err + } else if len(raw) == 0 { + return 0, NotFound + } + var blockNumber hexutil.Uint64 + if err := json.Unmarshal(raw, &blockNumber); err != nil { + return 0, err + } + return blockNumber, nil +} + +// BlockByHash returns the given full block. +// +// Note that loading full blocks requires two requests. Use HeaderByHash +// if you don't need all transactions or uncle headers. +func (c *Client) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + return c.getBlock(ctx, "hmy_getBlockByHash", hash, true) +} + +// BlockByNumber returns a block from the current canonical chain. If number is nil, the +// latest known block is returned. +// +// Note that loading full blocks requires two requests. Use HeaderByNumber +// if you don't need all transactions or uncle headers. +func (c *Client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + return c.getBlock(ctx, "hmy_getBlockByNumber", toBlockNumArg(number), true) +} + +// NetworkID returns the network ID (also known as the chain ID) for this chain. +func (c *Client) NetworkID(ctx context.Context) (*big.Int, error) { + version := new(big.Int) + var ver string + if err := c.c.CallContext(ctx, &ver, "net_version"); err != nil { + return nil, err + } + if _, ok := version.SetString(ver, 10); !ok { + return nil, fmt.Errorf("invalid net_version result %q", ver) + } + return version, nil +} + +func (c *Client) getBlock(ctx context.Context, method string, args ...interface{}) (*types.Block, error) { + var raw json.RawMessage + err := c.c.CallContext(ctx, &raw, method, args...) + if err != nil { + return nil, err + } else if len(raw) == 0 { + return nil, NotFound + } + // Decode header and transactions. + var head *types.Header + var body rpcBlock + if err := json.Unmarshal(raw, &head); err != nil { + return nil, err + } + if err := json.Unmarshal(raw, &body); err != nil { + return nil, err + } + // Quick-verify transaction. This mostly helps with debugging the server. + if head.TxHash == types.EmptyRootHash && len(body.Transactions) > 0 { + return nil, fmt.Errorf("server returned non-empty transaction list but block header indicates no transactions") + } + if head.TxHash != types.EmptyRootHash && len(body.Transactions) == 0 { + return nil, fmt.Errorf("server returned empty transaction list but block header indicates transactions") + } + // Load uncles because they are not included in the block response. + var uncles []*types.Header + if len(body.UncleHashes) > 0 { + uncles = make([]*types.Header, len(body.UncleHashes)) + reqs := make([]rpc.BatchElem, len(body.UncleHashes)) + for i := range reqs { + reqs[i] = rpc.BatchElem{ + Method: "eth_getUncleByBlockHashAndIndex", + Args: []interface{}{body.Hash, hexutil.EncodeUint64(uint64(i))}, + Result: &uncles[i], + } + } + if err := c.c.BatchCallContext(ctx, reqs); err != nil { + return nil, err + } + for i := range reqs { + if reqs[i].Error != nil { + return nil, reqs[i].Error + } + if uncles[i] == nil { + return nil, fmt.Errorf("got null header for uncle %d of block %x", i, body.Hash[:]) + } + } + } + // Fill the sender cache of transactions in the block. + txs := make([]*types.Transaction, len(body.Transactions)) + for i, tx := range body.Transactions { + if tx.From != nil { + setSenderFromServer(tx.tx, *tx.From, body.Hash) + } + txs[i] = tx.tx + } + return types.NewBlockWithHeader(head).WithBody(txs, uncles), nil +} + +func toBlockNumArg(number *big.Int) string { + if number == nil { + return "latest" + } + return hexutil.EncodeBig(number) +} diff --git a/hmyclient/signer.go b/hmyclient/signer.go new file mode 100644 index 000000000..3ab6070e8 --- /dev/null +++ b/hmyclient/signer.go @@ -0,0 +1,42 @@ +package hmyclient + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/core/types" +) + +var errNotCached = errors.New("sender not cached") + +// senderFromServer is a types.Signer that remembers the sender address returned by the RPC +// server. It is stored in the transaction's sender address cache to avoid an additional +// request in TransactionSender. +type senderFromServer struct { + addr common.Address + blockhash common.Hash +} + +func setSenderFromServer(tx *types.Transaction, addr common.Address, block common.Hash) { + // Use types.Sender for side-effect to store our signer into the cache. + types.Sender(&senderFromServer{addr, block}, tx) +} +func (s *senderFromServer) Equal(other types.Signer) bool { + os, ok := other.(*senderFromServer) + return ok && os.blockhash == s.blockhash +} + +func (s *senderFromServer) Sender(tx *types.Transaction) (common.Address, error) { + if s.blockhash == (common.Hash{}) { + return common.Address{}, errNotCached + } + return s.addr, nil +} + +func (s *senderFromServer) Hash(tx *types.Transaction) common.Hash { + panic("can't sign with senderFromServer") +} +func (s *senderFromServer) SignatureValues(tx *types.Transaction, sig []byte) (R, S, V *big.Int, err error) { + panic("can't sign with senderFromServer") +} diff --git a/hmyclient/type.go b/hmyclient/type.go new file mode 100644 index 000000000..dffa64bcf --- /dev/null +++ b/hmyclient/type.go @@ -0,0 +1,23 @@ +package hmyclient + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/core/types" +) + +type rpcBlock struct { + Hash common.Hash `json:"hash"` + Transactions []rpcTransaction `json:"transactions"` + UncleHashes []common.Hash `json:"uncles"` +} + +type rpcTransaction struct { + tx *types.Transaction + txExtraInfo +} + +type txExtraInfo struct { + BlockNumber *string `json:"blockNumber,omitempty"` + BlockHash *common.Hash `json:"blockHash,omitempty"` + From *common.Address `json:"from,omitempty"` +}