diff --git a/CHANGELOG.md b/CHANGELOG.md index 258e33a..edde7eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index 27f9cd3..f8e233b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.0 +1.9.0 diff --git a/neo/client.go b/neo/client.go index 0fba90e..6d1114e 100644 --- a/neo/client.go +++ b/neo/client.go @@ -2,6 +2,8 @@ package neo import ( "encoding/hex" + "errors" + "fmt" "net" "net/url" @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -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 } @@ -160,7 +180,7 @@ 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 } @@ -168,9 +188,43 @@ func (c Client) GetUnconfirmedTransactions() ([]string, error) { 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 } @@ -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 } diff --git a/neo/client_test.go b/neo/client_test.go index eab648d..ab1e630 100644 --- a/neo/client_test.go +++ b/neo/client_test.go @@ -8,20 +8,27 @@ 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) }) @@ -29,7 +36,8 @@ func TestClient(t *testing.T) { 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) { @@ -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) { @@ -65,9 +74,10 @@ 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) }) @@ -75,7 +85,8 @@ func TestClient(t *testing.T) { 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) { @@ -91,9 +102,10 @@ 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) }) @@ -101,7 +113,8 @@ func TestClient(t *testing.T) { 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", @@ -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) { @@ -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) { @@ -150,18 +165,20 @@ 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) }) @@ -169,8 +186,8 @@ func TestClient(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) }) } @@ -178,7 +195,8 @@ func TestClient(t *testing.T) { 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) { @@ -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) }) diff --git a/neo/fixtures_test.go b/neo/fixtures_test.go index 36c4195..e2d2dfc 100644 --- a/neo/fixtures_test.go +++ b/neo/fixtures_test.go @@ -72,10 +72,10 @@ var ( }{ { asset: "0xc56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b", - hash: "0xd30e2fe0bc3ecb1fde65f611a7f827268535ab19d2d2647d9984f35719185945", + hash: "0x96fd0fc8a3cddbaac868647624c32cb8ac27f35cf249e4e6c8123601113d4017", id: "1", index: 0, - value: "6", + value: "2", }, } diff --git a/neo/helper_test.go b/neo/helper_test.go deleted file mode 100644 index c2beddc..0000000 --- a/neo/helper_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package neo_test - -import ( - "fmt" - - "github.com/CityOfZion/neo-go-sdk/neo" -) - -const ( - neoNodeURI = "http://seed%d.neo.org:10332" -) - -func selectTestNode() string { - var nodeURI string - - for i := 1; i <= 5; i++ { - uri := fmt.Sprintf(neoNodeURI, i) - client := neo.NewClient(uri) - - ok := client.Ping() - if ok { - nodeURI = uri - break - } - } - - if nodeURI == "" { - panic("No available nodes for testing.") - } - - return nodeURI -}