Skip to content

Commit

Permalink
Select Best Node (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
Charlie Revett authored Jan 22, 2018
1 parent 164a69c commit 411fb63
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 73 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this
project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## v1.9.0 - 2018-01-22

### Added

- New function to select the best node from a array of node URIs.

```golang
client, err := neo.NewClientUsingMultipleNodes(
[]string{
"http://seed1.neo.org:10332",
"http://seed2.neo.org:10332",
"http://seed3.neo.org:10332",
},
)

err = client.SelectBestNode()
```

## v1.8.0 - 2017-10-27

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.8.0
1.9.0
84 changes: 69 additions & 15 deletions neo/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package neo

import (
"encoding/hex"
"errors"
"fmt"
"net"
"net/url"

Expand All @@ -12,22 +14,40 @@ import (
type (
// Client is the entrypoint for the package, it is used to carry out all actions.
Client struct {
NodeURI string
Node string
nodeURIs []string
}
)

// NewClient creates a new Client struct using the providing NodeURI argument.
// NewClient creates a new Client struct, with a single node URI.
func NewClient(nodeURI string) Client {
return Client{
NodeURI: nodeURI,
Node: nodeURI,
nodeURIs: []string{nodeURI},
}
}

// NewClientUsingMultipleNodes creates a new Client struct, and allows multiple node URIs
// to be passed in. Before the Client struct is returned, each node is queried to determine
// its block height. The node with the highest block count is chosen.
func NewClientUsingMultipleNodes(nodeURIs []string) (*Client, error) {
if len(nodeURIs) == 0 {
return nil, errors.New("Length of 'nodeURIs' argument must be greater than 0")
}

client := Client{
nodeURIs: nodeURIs,
}

client.SelectBestNode()
return &client, nil
}

// GetBestBlockHash returns the hash of the best block in the chain.
func (c Client) GetBestBlockHash() (string, error) {
var response response.String

err := executeRequest("getbestblockhash", nil, c.NodeURI, &response)
err := executeRequest("getbestblockhash", nil, c.Node, &response)
if err != nil {
return "", err
}
Expand All @@ -43,7 +63,7 @@ func (c Client) GetBlockByHash(hash string) (*models.Block, error) {
}
var response response.Block

err := executeRequest("getblock", requestBodyParams, c.NodeURI, &response)
err := executeRequest("getblock", requestBodyParams, c.Node, &response)
if err != nil {
return nil, err
}
Expand All @@ -59,7 +79,7 @@ func (c Client) GetBlockByIndex(index int64) (*models.Block, error) {
}
var response response.Block

err := executeRequest("getblock", requestBodyParams, c.NodeURI, &response)
err := executeRequest("getblock", requestBodyParams, c.Node, &response)
if err != nil {
return nil, err
}
Expand All @@ -71,7 +91,7 @@ func (c Client) GetBlockByIndex(index int64) (*models.Block, error) {
func (c Client) GetBlockCount() (int64, error) {
var response response.Integer

err := executeRequest("getblockcount", nil, c.NodeURI, &response)
err := executeRequest("getblockcount", nil, c.Node, &response)
if err != nil {
return 0, err
}
Expand All @@ -87,7 +107,7 @@ func (c Client) GetBlockHash(index int64) (string, error) {
}
var response response.String

err := executeRequest("getblockhash", requestBodyParams, c.NodeURI, &response)
err := executeRequest("getblockhash", requestBodyParams, c.Node, &response)
if err != nil {
return "", err
}
Expand All @@ -99,7 +119,7 @@ func (c Client) GetBlockHash(index int64) (string, error) {
func (c Client) GetConnectionCount() (int64, error) {
var response response.Integer

err := executeRequest("getconnectioncount", nil, c.NodeURI, &response)
err := executeRequest("getconnectioncount", nil, c.Node, &response)
if err != nil {
return 0, err
}
Expand All @@ -115,7 +135,7 @@ func (c Client) GetStorage(scriptHash string, storageKey string) (string, error)
}
var response response.String

err := executeRequest("getstorage", requestBodyParams, c.NodeURI, &response)
err := executeRequest("getstorage", requestBodyParams, c.Node, &response)
if err != nil {
return "", err
}
Expand All @@ -131,7 +151,7 @@ func (c Client) GetTransaction(hash string) (*models.Transaction, error) {
}
var response response.Transaction

err := executeRequest("getrawtransaction", requestBodyParams, c.NodeURI, &response)
err := executeRequest("getrawtransaction", requestBodyParams, c.Node, &response)
if err != nil {
return nil, err
}
Expand All @@ -147,7 +167,7 @@ func (c Client) GetTransactionOutput(hash string, index int64) (*models.Vout, er
}
var response response.Vout

err := executeRequest("gettxout", requestBodyParams, c.NodeURI, &response)
err := executeRequest("gettxout", requestBodyParams, c.Node, &response)
if err != nil {
return nil, err
}
Expand All @@ -160,17 +180,51 @@ func (c Client) GetTransactionOutput(hash string, index int64) (*models.Vout, er
func (c Client) GetUnconfirmedTransactions() ([]string, error) {
var response response.StringArray

err := executeRequest("getrawmempool", nil, c.NodeURI, &response)
err := executeRequest("getrawmempool", nil, c.Node, &response)
if err != nil {
return nil, err
}

return response.Result, nil
}

// SelectBestNode selects the best node to use for RPC calls. If there is a single
// node URI then that will be used. If there are 2 or more then each node is called
// and the block count is compared. The node with the heighest block count is used.
func (c *Client) SelectBestNode() error {
if len(c.nodeURIs) == 1 {
c.Node = c.nodeURIs[0]
return nil
}

var bestNode string
highestBlock := int64(0)

for _, nodeURI := range c.nodeURIs {
tempClient := NewClient(nodeURI)

blockCount, err := tempClient.GetBlockCount()
if err != nil {
continue
}

if blockCount > highestBlock {
highestBlock = blockCount
bestNode = nodeURI
}
}

if bestNode == "" {
return fmt.Errorf("Unable to communicate with any nodes")
}

c.Node = bestNode
return nil
}

// Ping checks if the node is online.
func (c Client) Ping() bool {
parsedURI, err := url.Parse(c.NodeURI)
parsedURI, err := url.Parse(c.Node)
if err != nil {
return false
}
Expand All @@ -192,7 +246,7 @@ func (c Client) ValidateAddress(address string) (bool, error) {
}
var response response.StringMap

err := executeRequest("validateaddress", requestBodyParams, c.NodeURI, &response)
err := executeRequest("validateaddress", requestBodyParams, c.Node, &response)
if err != nil {
return false, err
}
Expand Down
65 changes: 42 additions & 23 deletions neo/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,36 @@ import (
)

func TestClient(t *testing.T) {
nodeURI := selectTestNode()
nodes := []string{
"http://seed5.neo.org:10332",
"http://seed4.neo.org:10332",
"http://seed3.neo.org:10332",
"http://seed2.neo.org:10332",
"http://seed1.neo.org:10332",
}

t.Run("NewClient()", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client := neo.NewClient(nodes[0])

assert.Equal(t, nodeURI, client.NodeURI)
assert.Equal(t, nodes[0], client.Node)
assert.IsType(t, neo.Client{}, client)
})

t.Run(".GetBestBlockHash()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
blockHash, err := client.GetBestBlockHash()
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

blockHash, err := client.GetBestBlockHash()
assert.NoError(t, err)
assert.NotEmpty(t, blockHash)
})
})

t.Run(".GetBlockByHash()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

for _, testBlock := range testBlocks {
t.Run(testBlock.id, func(t *testing.T) {
Expand All @@ -47,7 +55,8 @@ func TestClient(t *testing.T) {

t.Run(".GetBlockByIndex()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

for _, testBlock := range testBlocks {
t.Run(testBlock.id, func(t *testing.T) {
Expand All @@ -65,17 +74,19 @@ func TestClient(t *testing.T) {

t.Run(".GetBlockCount()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
blockCount, err := client.GetBlockCount()
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

blockCount, err := client.GetBlockCount()
assert.NoError(t, err)
assert.NotEmpty(t, blockCount)
})
})

t.Run(".GetBlockHash()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

for _, testBlockHash := range testBlockHashes {
t.Run(testBlockHash.id, func(t *testing.T) {
Expand All @@ -91,17 +102,19 @@ func TestClient(t *testing.T) {

t.Run(".GetConnectionCount()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
blockCount, err := client.GetConnectionCount()
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

blockCount, err := client.GetConnectionCount()
assert.NoError(t, err)
assert.NotEmpty(t, blockCount)
})
})

t.Run(".GetStorage()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

storage, err := client.GetStorage(
"0xecc6b20d3ccac1ee9ef109af5a7cdb85706b1df9",
Expand All @@ -115,7 +128,8 @@ func TestClient(t *testing.T) {

t.Run(".GetTransaction()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

for _, testTransaction := range testTransactions {
t.Run(testTransaction.id, func(t *testing.T) {
Expand All @@ -131,7 +145,8 @@ func TestClient(t *testing.T) {

t.Run(".GetTransactionOutput()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

for _, testTransactionOutput := range testTransactionOutputs {
t.Run(testTransactionOutput.id, func(t *testing.T) {
Expand All @@ -150,35 +165,38 @@ func TestClient(t *testing.T) {

t.Run(".GetUnconfirmedTransactions()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
_, err := client.GetUnconfirmedTransactions()
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

_, err = client.GetUnconfirmedTransactions()
assert.NoError(t, err)
})
})

t.Run(".Ping()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
ok := client.Ping()
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

ok := client.Ping()
assert.True(t, ok)
})

t.Run("SadCase", func(t *testing.T) {
for _, testPing := range testPings {
t.Run(testPing.description, func(t *testing.T) {
client := neo.NewClient(testPing.uri)
ok := client.Ping()

ok := client.Ping()
assert.False(t, ok)
})
}
})

t.Run(".ValidateAddress()", func(t *testing.T) {
t.Run("HappyCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

for _, testAccount := range testAccounts {
t.Run(testAccount.publicAddress, func(t *testing.T) {
Expand All @@ -191,9 +209,10 @@ func TestClient(t *testing.T) {
})

t.Run("SadCase", func(t *testing.T) {
client := neo.NewClient(nodeURI)
isValid, err := client.ValidateAddress("wake-up-neo")
client, err := neo.NewClientUsingMultipleNodes(nodes)
assert.NoError(t, err)

isValid, err := client.ValidateAddress("wake-up-neo")
assert.NoError(t, err)
assert.False(t, isValid)
})
Expand Down
Loading

0 comments on commit 411fb63

Please sign in to comment.