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

Go kosu/rpc #174

Merged
merged 6 commits into from Jul 23, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -144,6 +144,9 @@ To start a single-node Kosu development network as a validator, and initialize a
./kosud --init --web3 wss://ethnet.zaidan.io/ws/ropsten
```

To start the `JSON-RPC` bridge and interact with kosud use the `kosud rpc` sub-command.
By default the HTTP and WS endpoints are binded to ports `14341` and `14342` repectively.

The command-line interface will also be built (see `kosu-cli help` for all commands).

```
@@ -3,6 +3,7 @@ package abci
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
@@ -21,6 +22,8 @@ const (
KOSUHOME = ".kosu"
)

var errHomeDirNotFound = errors.New("homedir does not exists! Did you run the init command?")

// DefaultHomeDir is the default full path used to store config and data
var DefaultHomeDir = os.ExpandEnv(fmt.Sprintf("$HOME/%s", KOSUHOME))

@@ -31,7 +34,7 @@ func LoadConfig(homedir string) (*config.Config, error) {
}

if !common.FileExists(filepath.Join(homedir, "config", "config.toml")) {
return nil, fmt.Errorf("missing homedir! Did you run the init command?")
return nil, errHomeDirNotFound
}

// Have a config file, load it
@@ -43,7 +46,7 @@ func LoadConfig(homedir string) (*config.Config, error) {
// I don't think this ever returns an err. It seems to create a default config if missing
err := viper.ReadInConfig()
if err != nil {
return nil, fmt.Errorf("missing homedir/config file. Did you run 'kosud --init'?")
return nil, errHomeDirNotFound
}

cfg := config.DefaultConfig()
@@ -12,6 +12,7 @@ import (
"github.com/tendermint/tendermint/libs/log"

"go-kosu/abci"
"go-kosu/rpc"
"go-kosu/witness"
)

@@ -96,8 +97,8 @@ func main() {
}
},
}
rootCmd.Flags().StringVar(&cfg.Home, "home", "~/.kosu", "directory for config and data")
rootCmd.Flags().BoolVar(&cfg.Debug, "debug", false, "enable debuging")
rootCmd.PersistentFlags().StringVar(&cfg.Home, "home", "~/.kosu", "directory for config and data")
rootCmd.PersistentFlags().BoolVar(&cfg.Debug, "debug", false, "enable debuging")
rootCmd.Flags().StringVar(&cfg.Web3, "web3", "wss://ethnet.zaidan.io/ws/kosu", "web3 provider URL")
rootCmd.Flags().BoolVar(&cfg.Init, "init", false, "initializes directory like 'tendermint init' does")

@@ -116,6 +117,8 @@ func main() {
}
})

rootCmd.AddCommand(rpc.NewCommand())

if err := rootCmd.Execute(); err != nil {
stdlog.Fatal(err)
}
@@ -0,0 +1,55 @@
package rpc

import (
"context"

"github.com/ethereum/go-ethereum/rpc"
)

type Client struct {
rpc *rpc.Client
}

func DialInProc(srv *rpc.Server) *Client {
return &Client{
rpc: rpc.DialInProc(srv),
}
}

func (c *Client) Subscribe(ctx context.Context, fn func(interface{}), query string) error {
ch := make(chan interface{})
args := []interface{}{"subscribe", query}
sub, err := c.rpc.Subscribe(ctx, "kosu", ch, args...)
if err != nil {
return err
}

go func() {
defer close(ch)
defer sub.Unsubscribe()

for {
select {
case <-ctx.Done():
return
case <-sub.Err():
return
case i := <-ch:
fn(i)
}
}
}()
return nil
}

func (c *Client) Call(result interface{}, ns, method string, args ...interface{}) error {
return c.rpc.Call(result, ns+"_"+method, args...)
}

func (c *Client) LatestHeight() (int64, error) {
var latestHeight int64
if err := c.Call(&latestHeight, "kosu", "latestHeight"); err != nil {
return 0, err
}
return latestHeight, nil
}
@@ -0,0 +1,104 @@
package rpc

import (
"errors"
"fmt"
"go-kosu/abci"
"log"
"net/http"
"sync"

"github.com/ethereum/go-ethereum/rpc"
"github.com/spf13/cobra"
)

// NewCommand returns a new cobra.Command to be attached as a sub-command
func NewCommand() *cobra.Command {
var (
url string

http bool
httpPort int

ws bool
wsPort int

key []byte
)
cmd := &cobra.Command{
Use: "rpc",
Short: "starts the rpc bridge",
Long: "The RPC bridge exposes a set of kosud functionalities over JSON-RPC 2.0",
PreRunE: func(cmd *cobra.Command, args []string) error {
if http == false && ws == false {
return errors.New("both `--ws` and `--http` where false, you need to enable at least one")
}

var homeDir string
if home := cmd.Flag("home"); home == nil {
homeDir = "~/kosu"
} else {
homeDir = home.Value.String()
}

var err error
key, err = abci.LoadPrivateKey(homeDir)
if err != nil {
return err
}

return nil
},
Run: func(cmd *cobra.Command, args []string) {
client := abci.NewHTTPClient(url, key)
srv := NewServer(client)

wg := sync.WaitGroup{}
if http == true {
wg.Add(1)
go func() {
defer wg.Done()
if err := startHTTP(srv, httpPort); err != nil {
log.Printf("http: %s", err)
}
}()
}

if ws == true {
wg.Add(1)
go func() {
defer wg.Done()
if err := startWS(srv, wsPort); err != nil {
log.Printf("ws: %s", err)
}
}()
}
wg.Wait()
},
}

cmd.Flags().StringVar(&url, "url", "http://localhost:26657", "URL exposed by kosud")
cmd.Flags().BoolVar(&http, "http", true, "Starts the HTTP server")
cmd.Flags().IntVar(&httpPort, "http-port", 14341, "HTTP server listening port")

cmd.Flags().BoolVar(&ws, "ws", true, "Starts the WebSocket server")
cmd.Flags().IntVar(&wsPort, "ws-port", 14342, "WebSocket server listening port")

return cmd
}

func startHTTP(srv *rpc.Server, port int) error {
bind := fmt.Sprintf(":%d", port)
log.Printf("Starting HTTP server on %s", bind)
return http.ListenAndServe(bind, srv)
}

func startWS(srv *rpc.Server, port int) error {
bind := fmt.Sprintf(":%d", port)
log.Printf("Starting WS server on %s", bind)

return http.ListenAndServe(
bind,
srv.WebsocketHandler([]string{"*"}),
)
}
@@ -0,0 +1,44 @@
package main

import (
"encoding/json"
"fmt"
"go/doc"
"go/parser"
"go/token"
"log"
"os"
)

type DocEntry struct {
Method string `json:"method"`
Text string `json:"text"`
}

func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: doctool <path>")
return
}

fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, os.Args[1], nil, parser.ParseComments)
if err != nil {
log.Fatal(err)
}

docs := []DocEntry{}
pkg := doc.New(pkgs["rpc"], os.Args[1], doc.AllDecls)
for _, t := range pkg.Types {
for _, m := range t.Methods {
docs = append(docs, DocEntry{Method: m.Name, Text: m.Doc})
}
}

text, err := json.MarshalIndent(docs, "", " ")
if err != nil {
log.Fatal(err)
}

fmt.Printf("%s\n", text)
}
@@ -0,0 +1,13 @@
package rpc

import (
"go-kosu/abci"

"github.com/ethereum/go-ethereum/rpc"
)

func NewServer(abci *abci.Client) *rpc.Server {
srv := rpc.NewServer()
srv.RegisterName("kosu", &Service{abci: abci})
return srv
}
@@ -0,0 +1,45 @@
package rpc

import (
"context"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"go-kosu/abci"
"go-kosu/tests"

"github.com/tendermint/tendermint/libs/db"
)

func TestRPCLatestHeight(t *testing.T) {
_, closer := tests.StartServer(t, db.NewMemDB())
defer closer()
client := DialInProc(
NewServer(
abci.NewHTTPClient("http://localhost:26657", nil),
),
)

// Get the initial (prior the first block is mined)
latest, err := client.LatestHeight()
require.NoError(t, err)
assert.EqualValues(t, 0, latest)

ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
fn := func(i interface{}) {
// this is invoked when a block is mined
latest, err := client.LatestHeight()
require.NoError(t, err)
assert.EqualValues(t, 1, latest)

cancel()
}

err = client.Subscribe(ctx, fn, "tm.event = 'NewBlock'")
require.NoError(t, err)

<-ctx.Done()
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.