├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── actions.go ├── client.go ├── error.go ├── examples ├── actionload │ └── main.go └── tableload │ └── main.go ├── go.mod ├── go.sum ├── listening.go ├── mdl ├── v0 │ └── type.go └── v1 │ ├── dbop.go │ ├── dtrxop.go │ ├── ramop.go │ ├── tableop.go │ └── transaction.go ├── message.go ├── message_test.go ├── name.go ├── progress.go ├── registry.go ├── tables.go └── unlisten.go /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | .idea 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10.x" 5 | - "1.11.x" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 dfuse.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | eosws Go bindings (from the dfuse API) 2 | -------------------------------------- 3 | 4 | Websocket consumer for the https://dfuse.io API on EOS networks. 5 | 6 | ## Connecting 7 | 8 | ```go 9 | jwt, exp, err := eosws.Auth("server_1234567....") 10 | if err != nil { 11 | log.Fatalf("cannot get auth token: %s", err.Error()) 12 | } 13 | time.AfterFunc(time.Until(exp), log.Println("JWT is now expired, renew it before reconnecting client")) // make sure that you handle updating your JWT 14 | 15 | client, err := eosws.New("wss://mainnet.eos.dfuse.io/v1/stream", jwt, "https://origin.example.com") 16 | if err != nil { 17 | log.Fatalf("cannot connect to dfuse endpoint: %s", err.Error()) 18 | } 19 | ``` 20 | 21 | ## Sending requests 22 | 23 | ```go 24 | ga := &eosws.GetActionTraces{ 25 | ga := &eosws.GetActionTraces{ 26 | ReqID: "myreq1", 27 | StartBlock: -5, 28 | Listen: true, 29 | } 30 | } 31 | ga.Data.Accounts = "eosio" 32 | ga.Data.ActionNames = "onblock" 33 | err = client.Send(ga) 34 | if err != nil { 35 | log.Fatalf("error sending request") 36 | } 37 | ``` 38 | 39 | ## Reading responses 40 | 41 | ```go 42 | for { 43 | msg, err := client.Read() 44 | errorCheck("reading message", err) 45 | 46 | switch m := msg.(type) { 47 | case *eosws.ActionTrace: 48 | fmt.Println(m.Data.Trace) 49 | default: 50 | fmt.Println("Unsupported message", m) 51 | break 52 | } 53 | } 54 | ``` 55 | 56 | ## Examples 57 | 58 | See `examples` folder 59 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func init() { 8 | RegisterIncomingMessage("action_trace", ActionTrace{}) 9 | RegisterOutgoingMessage("get_action_traces", GetActionTraces{}) 10 | } 11 | 12 | type GetActionTraces struct { 13 | CommonOut 14 | 15 | Data struct { 16 | Receivers string `json:"receivers,omitempty"` 17 | Accounts string `json:"accounts"` 18 | ActionNames string `json:"action_names,omitempty"` 19 | Receiver string `json:"receiver,omitempty"` 20 | Account string `json:"account,omitempty"` 21 | ActionName string `json:"action_name,omitempty"` 22 | WithDBOps bool `json:"with_dbops"` 23 | WithRAMOps bool `json:"with_ramops"` 24 | WithDTrxOps bool `json:"with_dtrxops"` 25 | WithInlineTraces bool `json:"with_inline_traces"` 26 | } `json:"data"` 27 | } 28 | 29 | type ActionTrace struct { 30 | CommonIn 31 | Data struct { 32 | BlockNum uint32 `json:"block_num"` 33 | BlockID string `json:"block_id"` 34 | TransactionID string `json:"trx_id"` 35 | ActionIndex int `json:"idx"` 36 | ActionDepth int `json:"depth"` 37 | Trace json.RawMessage `json:"trace"` 38 | DBOps json.RawMessage `json:"dbops,omitempty"` 39 | RAMOps json.RawMessage `json:"ramops,omitempty"` 40 | DTrxOps json.RawMessage `json:"dtrxops,omitempty"` 41 | } `json:"data"` 42 | } 43 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "reflect" 9 | "sync" 10 | "time" 11 | 12 | "github.com/gorilla/websocket" 13 | ) 14 | 15 | func Auth(apiKey string) (token string, expiration time.Time, err error) { 16 | return AuthWithURL(apiKey, "https://auth.dfuse.io/v1/auth/issue") 17 | } 18 | 19 | func AuthWithURL(apiKey string, authServiceURL string) (token string, expiration time.Time, err error) { 20 | resp, err := http.Post(authServiceURL, "application/json", bytes.NewBuffer([]byte(fmt.Sprintf(`{"api_key":"%s"}`, apiKey)))) 21 | if err != nil { 22 | return token, expiration, err 23 | } 24 | if resp.StatusCode != 200 { 25 | return token, expiration, fmt.Errorf("wrong status code from Auth: %d", resp.StatusCode) 26 | } 27 | var result apiToJWTResp 28 | err = json.NewDecoder(resp.Body).Decode(&result) 29 | if err != nil { 30 | return 31 | } 32 | return result.Token, time.Unix(result.ExpiresAt, 0), nil 33 | } 34 | 35 | func New(endpoint, token, origin string) (*Client, error) { 36 | endpoint = fmt.Sprintf("%s?token=%s", endpoint, token) 37 | reqHeaders := http.Header{"Origin": []string{origin}} 38 | conn, resp, err := websocket.DefaultDialer.Dial(endpoint, reqHeaders) 39 | if err != nil { 40 | if resp != nil { 41 | return nil, fmt.Errorf("error, returned status=%d: %s: %s", resp.StatusCode, err, resp.Header.Get("X-Websocket-Handshake-Error")) 42 | } 43 | return nil, fmt.Errorf("error dialing to endpoint: %s", err) 44 | } 45 | c := &Client{ 46 | conn: conn, 47 | incoming: make(chan interface{}, 1000), 48 | } 49 | 50 | go c.readLoop() 51 | 52 | return c, nil 53 | } 54 | 55 | type Client struct { 56 | conn *websocket.Conn 57 | readError error 58 | incoming chan interface{} 59 | writeLock sync.Mutex 60 | } 61 | 62 | // 63 | 64 | func (c *Client) Read() (interface{}, error) { 65 | select { 66 | case el := <-c.incoming: 67 | if el == nil { 68 | return nil, c.readError 69 | } 70 | return el, nil 71 | } 72 | } 73 | 74 | func (c *Client) Close() error { 75 | return c.conn.Close() 76 | } 77 | 78 | func (c *Client) readLoop() { 79 | var err error 80 | var msgType int 81 | var cnt []byte 82 | 83 | defer func() { 84 | c.readError = err 85 | close(c.incoming) 86 | c.conn.Close() 87 | }() 88 | 89 | for { 90 | _ = c.conn.SetReadDeadline(time.Now().Add(120 * time.Second)) 91 | msgType, cnt, err = c.conn.ReadMessage() 92 | if err != nil { 93 | // the `defer` will return the `err` here.. 94 | // LOG error, close write, shutdown client, store the error, whatever 95 | return 96 | } 97 | if msgType != websocket.TextMessage { 98 | fmt.Println("eosws client: invalid incoming message type", msgType) 99 | // Server should not send messages other than json-encoded text messages 100 | continue 101 | } 102 | 103 | var inspect CommonIn 104 | err = json.Unmarshal(cnt, &inspect) 105 | if err != nil { 106 | fmt.Println("eosws client: error unmarshaling incoming message:", err) 107 | // LOG THERE WAS AN ERROR IN THE INCOMING JSON: 108 | continue 109 | } 110 | 111 | if inspect.Type == "ping" { 112 | pong := bytes.Replace(cnt, []byte(`"ping"`), []byte(`"pong"`), 1) 113 | //fmt.Println("eosws client: sending pong", string(pong)) 114 | _ = c.conn.WriteMessage(websocket.TextMessage, pong) 115 | continue 116 | } 117 | 118 | objType := IncomingMessageMap[inspect.Type] 119 | if objType == nil { 120 | fmt.Printf("eosws client: received unsupported incoming message type %q\n", inspect.Type) 121 | // LOG: incoming message not supported, do we pass the raw JSON object? 122 | continue 123 | } 124 | 125 | obj := reflect.New(objType).Interface() 126 | err = json.Unmarshal(cnt, &obj) 127 | if err != nil { 128 | 129 | fmt.Printf("Error unmarshalling :%q :%s\n", inspect.Type, err) 130 | fmt.Println("Data: ", string(cnt)) 131 | // LOG or push an error: "cannot unmarshal incoming message into our struct 132 | //emitError(inspect.ReqID, "unmarshal_message", err, wsmsg.M{"type": inspect.Type}) 133 | continue 134 | } 135 | 136 | c.incoming <- obj 137 | } 138 | } 139 | 140 | // Send to the websocket, one of the messages registered through 141 | // `RegisterOutboundMessage`. 142 | func (c *Client) Send(msg OutgoingMessager) error { 143 | setType(msg) 144 | cnt, err := json.Marshal(msg) 145 | if err != nil { 146 | return fmt.Errorf("marshalling message %T (%#v): %s", msg, msg, err) 147 | } 148 | 149 | c.writeLock.Lock() 150 | defer c.writeLock.Unlock() 151 | if err := c.conn.WriteMessage(websocket.TextMessage, cnt); err != nil { 152 | return fmt.Errorf("writing message %T (%#v) to WS: %s", msg, msg, err) 153 | } 154 | 155 | return nil 156 | } 157 | 158 | type apiToJWTResp struct { 159 | Token string `json:"token"` 160 | ExpiresAt int64 `json:"expires_at"` 161 | } 162 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | func init() { 4 | RegisterIncomingMessage("error", Error{}) 5 | } 6 | 7 | type Error struct { 8 | CommonIn 9 | 10 | Data struct { 11 | Code string `json:"code"` 12 | Message string `json:"message"` 13 | Details map[string]interface{} `json:"details"` 14 | } `json:"data"` 15 | } 16 | -------------------------------------------------------------------------------- /examples/actionload/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | eosws "github.com/dfuse-io/eosws-go" 10 | ) 11 | 12 | var dfuse_endpoint = "wss://mainnet.eos.dfuse.io/v1/stream" 13 | var origin = "https://origin.example.com" 14 | 15 | func main() { 16 | 17 | api_key := os.Getenv("EOSWS_API_KEY") 18 | if api_key == "" { 19 | log.Fatalf("please set your API key to environment variable EOSWS_API_KEY") 20 | } 21 | 22 | jwt, _, err := eosws.Auth(api_key) 23 | if err != nil { 24 | log.Fatalf("cannot get JWT token: %s", err.Error()) 25 | } 26 | 27 | client, err := eosws.New(dfuse_endpoint, jwt, origin) 28 | errorCheck("connecting to endpoint"+dfuse_endpoint, err) 29 | 30 | go func() { 31 | 32 | ga := &eosws.GetActionTraces{} 33 | ga.ReqID = "foo GetActions" 34 | ga.StartBlock = -350 35 | ga.Listen = true 36 | ga.WithProgress = 5 37 | ga.IrreversibleOnly = true 38 | ga.Data.Accounts = "eosio.token" 39 | ga.Data.ActionNames = "transfer" 40 | ga.Data.WithInlineTraces = true 41 | 42 | fmt.Printf("Sending `get_actions` message for accounts: %s and action names: %s", ga.Data.Accounts, ga.Data.ActionNames) 43 | err = client.Send(ga) 44 | errorCheck("sending get_actions", err) 45 | 46 | for { 47 | msg, err := client.Read() 48 | if err != nil { 49 | fmt.Println("DIED", err) 50 | return 51 | } 52 | 53 | switch m := msg.(type) { 54 | case *eosws.ActionTrace: 55 | fmt.Println("Block Num:", m.Data.BlockNum, m.Data.TransactionID) 56 | case *eosws.Progress: 57 | fmt.Println("Progress", m.Data.BlockNum) 58 | case *eosws.Listening: 59 | fmt.Println("listening...") 60 | default: 61 | fmt.Println("Unsupported message", m) 62 | } 63 | } 64 | }() 65 | 66 | time.Sleep(8 * time.Second) 67 | } 68 | 69 | func errorCheck(prefix string, err error) { 70 | if err != nil { 71 | log.Fatalf("ERROR: %s: %s\n", prefix, err) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/tableload/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | eosws "github.com/dfuse-io/eosws-go" 11 | ) 12 | 13 | var dfuse_endpoint = "wss://mainnet.eos.dfuse.io/v1/stream" 14 | var origin = "https://origin.example.com" 15 | 16 | func main() { 17 | 18 | api_key := os.Getenv("EOSWS_API_KEY") 19 | if api_key == "" { 20 | log.Fatalf("please set your API key to environment variable EOSWS_API_KEY") 21 | } 22 | 23 | jwt, _, err := eosws.Auth(api_key) 24 | if err != nil { 25 | log.Fatalf("cannot get JWT token: %s", err.Error()) 26 | } 27 | 28 | client, err := eosws.New(dfuse_endpoint, jwt, origin) 29 | errorCheck("connecting to endpoint"+dfuse_endpoint, err) 30 | 31 | go func() { 32 | 33 | ga := &eosws.GetTableRows{} 34 | ga.ReqID = "foo GetTableRows" 35 | ga.StartBlock = -3600 36 | ga.Listen = true 37 | ga.Fetch = true 38 | ga.WithProgress = 5 39 | ga.Data.JSON = true 40 | ga.Data.Code = "eosio.token" 41 | ga.Data.Scope = "eosio" 42 | ga.Data.Table = "accounts" 43 | 44 | fmt.Println("Sending `get_table_rows` message") 45 | err = client.Send(ga) 46 | errorCheck("sending get_table_rows", err) 47 | 48 | for { 49 | msg, err := client.Read() 50 | if err != nil { 51 | fmt.Println("DIED", err) 52 | return 53 | } 54 | 55 | switch m := msg.(type) { 56 | case *eosws.Progress: 57 | fmt.Println("Progress", m.Data.BlockNum) 58 | case *eosws.TableDelta: 59 | fmt.Printf("%d: %+v\n", m.Data.BlockNum, m.Data.DBOp) 60 | case *eosws.TableSnapshot: 61 | cnt, _ := json.Marshal(m) 62 | fmt.Println("Rows: ", string(cnt)) 63 | case *eosws.Listening: 64 | fmt.Println("listening...") 65 | default: 66 | fmt.Println("Unsupported message", m) 67 | } 68 | } 69 | }() 70 | 71 | timeout := 10 * time.Second 72 | time.Sleep(timeout) 73 | log.Println("done after", timeout) 74 | } 75 | 76 | func errorCheck(prefix string, err error) { 77 | if err != nil { 78 | log.Fatalf("ERROR: %s: %s\n", prefix, err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dfuse-io/eosws-go 2 | 3 | require ( 4 | github.com/eoscanada/eos-go v0.8.5 5 | github.com/gorilla/websocket v1.4.1 6 | github.com/pkg/errors v0.8.1 // indirect 7 | github.com/stretchr/testify v1.3.0 8 | github.com/tidwall/gjson v1.9.3 // indirect 9 | github.com/tidwall/sjson v1.0.4 // indirect 10 | go.uber.org/atomic v1.3.2 // indirect 11 | go.uber.org/multierr v1.1.0 // indirect 12 | go.uber.org/zap v1.9.1 // indirect 13 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 // indirect 14 | ) 15 | 16 | go 1.13 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/eoscanada/eos-go v0.8.5 h1:6A+hf3pEti8hKqkU/yBlj0Zn7JwOSGFhrcYW5h1rRkA= 4 | github.com/eoscanada/eos-go v0.8.5/go.mod h1:RKrm2XzZEZWxSMTRqH5QOyJ1fb/qKEjs2ix1aQl0sk4= 5 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 6 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 7 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 8 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 13 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 14 | github.com/tidwall/gjson v1.9.3 h1:hqzS9wAHMO+KVBBkLxYdkEeeFHuqr95GfClRLKlgK0E= 15 | github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 16 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 17 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 18 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 19 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 20 | github.com/tidwall/sjson v1.0.4 h1:UcdIRXff12Lpnu3OLtZvnc03g4vH2suXDXhBwBqmzYg= 21 | github.com/tidwall/sjson v1.0.4/go.mod h1:bURseu1nuBkFpIES5cz6zBtjmYeOQmEESshn7VpF15Y= 22 | go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= 23 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 24 | go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= 25 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 26 | go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= 27 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 28 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= 29 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | -------------------------------------------------------------------------------- /listening.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | func init() { 4 | RegisterIncomingMessage("listening", Listening{}) 5 | } 6 | 7 | type Listening struct { 8 | CommonOut 9 | Data struct { 10 | NextBlock uint32 `json:"next_block"` 11 | } `json:"data"` 12 | } 13 | -------------------------------------------------------------------------------- /mdl/v0/type.go: -------------------------------------------------------------------------------- 1 | package v0 2 | 3 | import "github.com/eoscanada/eos-go" 4 | 5 | type DBOp struct { 6 | Operation string `json:"op"` 7 | ActionIndex int `json:"action_idx"` 8 | OldPayer string `json:"opayer,omitempty"` 9 | NewPayer string `json:"npayer,omitempty"` 10 | TablePath string `json:"path"` 11 | OldData string `json:"old,omitempty"` 12 | NewData string `json:"new,omitempty"` 13 | 14 | chunks []string 15 | } 16 | 17 | type RAMOp struct { 18 | ActionIndex int `json:"action_idx"` 19 | EventID string `json:"event_id"` 20 | Family string `json:"family"` 21 | Action string `json:"action"` 22 | Operation string `json:"op"` 23 | Payer string `json:"payer"` 24 | Delta eos.Int64 `json:"delta"` 25 | Usage eos.Uint64 `json:"usage"` // new usage 26 | } 27 | 28 | type DTrxOp struct { 29 | Operation string `json:"op"` 30 | ActionIndex int `json:"action_idx"` 31 | Sender string `json:"sender"` 32 | SenderID string `json:"sender_id"` 33 | Payer string `json:"payer"` 34 | PublishedAt string `json:"published_at"` 35 | DelayUntil string `json:"delay_until"` 36 | ExpirationAt string `json:"expiration_at"` 37 | TransactionID string `json:"trx_id"` 38 | Transaction *eos.Transaction `json:"trx,omitempty"` 39 | } 40 | 41 | type TableOp struct { 42 | Operation string `json:"op"` 43 | ActionIndex int `json:"action_idx"` 44 | Payer string `json:"payer"` 45 | Path string `json:"path"` 46 | chunks []string 47 | } -------------------------------------------------------------------------------- /mdl/v1/dbop.go: -------------------------------------------------------------------------------- 1 | package mdl 2 | 3 | type DBOp struct { 4 | Op string `json:"op,omitempty"` 5 | ActionIndex int `json:"action_idx"` 6 | Account string `json:"account,omitempty"` 7 | Table string `json:"table,omitempty"` 8 | Scope string `json:"scope,omitempty"` 9 | Key string `json:"key,omitempty"` 10 | Old *DBRow `json:"old,omitempty"` 11 | New *DBRow `json:"new,omitempty"` 12 | } 13 | 14 | type DBRow struct { 15 | Payer string `json:"payer,omitempty"` 16 | Hex string `json:"hex,omitempty"` 17 | JSON interface{} `json:"json,omitempty"` 18 | Error string `json:"error,omitempty"` 19 | } 20 | -------------------------------------------------------------------------------- /mdl/v1/dtrxop.go: -------------------------------------------------------------------------------- 1 | package mdl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | eos "github.com/eoscanada/eos-go" 9 | ) 10 | 11 | type DTrxOp struct { 12 | Operation string `json:"op"` 13 | ActionIndex int `json:"action_idx"` 14 | Sender string `json:"sender"` 15 | SenderID string `json:"sender_id"` 16 | Payer string `json:"payer"` 17 | PublishedAt string `json:"published_at"` 18 | DelayUntil string `json:"delay_until"` 19 | ExpirationAt string `json:"expiration_at"` 20 | TrxID string `json:"trx_id"` 21 | Trx json.RawMessage `json:"trx,omitempty"` 22 | } 23 | 24 | type ExtDTrxOp struct { 25 | SourceTransactionID string `json:"src_trx_id"` 26 | BlockNum uint32 `json:"block_num"` 27 | BlockID string `json:"block_id"` 28 | BlockTime eos.BlockTimestamp `json:"block_time"` 29 | 30 | DTrxOp 31 | } 32 | 33 | func (d *DTrxOp) IsCreateOperation() bool { 34 | dOp := strings.ToLower(d.Operation) 35 | if dOp == "push_create" || dOp == "modify_create" || dOp == "create" { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (d *DTrxOp) IsCancelOperation() bool { 42 | dOp := strings.ToLower(d.Operation) 43 | if dOp == "modify_cancel" || dOp == "cancel" { 44 | return true 45 | } 46 | return false 47 | } 48 | 49 | func (d *DTrxOp) SignedTransaction() (transaction *eos.SignedTransaction, err error) { 50 | err = json.Unmarshal(d.Trx, &transaction) 51 | if err != nil { 52 | return nil, fmt.Errorf("unmarshall signedTransaction: %s", err) 53 | } 54 | return transaction, nil 55 | } 56 | -------------------------------------------------------------------------------- /mdl/v1/ramop.go: -------------------------------------------------------------------------------- 1 | package mdl 2 | 3 | type RAMOp struct { 4 | Operation string `json:"op"` 5 | Family string `json:"family"` 6 | Action string `json:"action"` 7 | ActionIndex int `json:"action_idx"` 8 | Payer string `json:"payer"` 9 | Delta int64 `json:"delta"` 10 | Usage uint64 `json:"usage"` // new usage 11 | } 12 | -------------------------------------------------------------------------------- /mdl/v1/tableop.go: -------------------------------------------------------------------------------- 1 | package mdl 2 | 3 | type TableOp struct { 4 | Operation string `json:"op"` 5 | ActionIndex int `json:"action_idx"` 6 | Payer string `json:"payer"` 7 | Path string `json:"path"` 8 | 9 | chunks []string 10 | } 11 | -------------------------------------------------------------------------------- /mdl/v1/transaction.go: -------------------------------------------------------------------------------- 1 | package mdl 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | eos "github.com/eoscanada/eos-go" 7 | "github.com/eoscanada/eos-go/ecc" 8 | ) 9 | 10 | // TransactionTrace maps to a `transaction_trace` in `chain/trace.hpp` 11 | type TransactionTrace struct { 12 | ID string `json:"id,omitempty"` 13 | BlockNum uint32 `json:"block_num"` 14 | BlockTime eos.BlockTimestamp `json:"block_time"` 15 | ProducerBlockID string `json:"producer_block_id"` 16 | Receipt eos.TransactionReceiptHeader `json:"receipt"` 17 | Elapsed int64 `json:"elapsed"` 18 | NetUsage uint64 `json:"net_usage"` 19 | Scheduled bool `json:"scheduled"` 20 | ActionTraces []*ActionTrace `json:"action_traces"` 21 | FailedDTrxTrace *TransactionTrace `json:"failed_dtrx_trace"` 22 | Except json.RawMessage `json:"except"` 23 | } 24 | 25 | // ActionReceipt corresponds to an `action_receipt` from `chain/action_receipt.hpp` 26 | type ActionReceipt struct { 27 | Receiver string `json:"receiver"` 28 | Digest string `json:"act_digest"` 29 | GlobalSequence eos.Uint64 `json:"global_sequence"` 30 | RecvSequence eos.Uint64 `json:"recv_sequence"` 31 | AuthSequence []json.RawMessage `json:"auth_sequence"` 32 | CodeSequence eos.Uint64 `json:"code_sequence"` 33 | ABISequence eos.Uint64 `json:"abi_sequence"` 34 | } 35 | 36 | // BaseActionTrace corresponds to a `base_action_trace` from `chain/trace.hpp` 37 | type BaseActionTrace struct { 38 | Receipt ActionReceipt `json:"receipt"` 39 | Action eos.Action `json:"act"` 40 | ContextFree bool `json:"context_free"` 41 | Elapsed int64 `json:"elapsed"` 42 | Console string `json:"console"` 43 | TransactionID string `json:"trx_id"` 44 | BlockNum uint32 `json:"block_num"` 45 | BlockTime eos.BlockTimestamp `json:"block_time"` 46 | ProducerBlockID *string `json:"producer_block_id,omitempty"` 47 | AccountRAMDeltas []*AccountRAMDelta `json:"account_ram_deltas"` 48 | Except json.RawMessage `json:"except"` 49 | } 50 | 51 | // AccountRAMDelta corresponds to an `account_delta` from `chain/trace.hpp` 52 | type AccountRAMDelta struct { 53 | Account eos.AccountName `json:"account"` 54 | Delta int64 `json:"delta"` 55 | } 56 | 57 | // ActionTrace corresponds to an `action_trace` from `chain/trace.hpp` 58 | type ActionTrace struct { 59 | ActionOrdinal uint32 `json:"action_ordinal"` 60 | CreatorActionOrdinal uint32 `json:"creator_action_ordinal"` 61 | ClosestUnnotifiedAncestorActionOrdinal uint32 `json:"closest_unnotified_ancestor_action_ordinal"` 62 | BaseActionTrace 63 | Receiver eos.AccountName `json:"receiver,omitempty"` 64 | InlineTraces []*ActionTrace `json:"inline_traces"` 65 | } 66 | 67 | type PermissionLevel struct { 68 | Actor string `json:"actor"` 69 | Permission string `json:"permission"` 70 | } 71 | 72 | type TransactionLifecycle struct { 73 | TransactionStatus string `json:"transaction_status"` 74 | ID string `json:"id"` 75 | Transaction *eos.SignedTransaction `json:"transaction"` 76 | ExecutionTrace *TransactionTrace `json:"execution_trace"` 77 | ExecutionBlockHeader *eos.BlockHeader `json:"execution_block_header"` 78 | DTrxOps []*DTrxOp `json:"dtrxops"` 79 | CreationTree CreationFlatTree `json:"creation_tree"` 80 | DBOps []*DBOp `json:"dbops"` 81 | RAMOps []*RAMOp `json:"ramops"` 82 | TableOps []*TableOp `json:"tableops"` 83 | PubKeys []*ecc.PublicKey `json:"pub_keys"` 84 | CreatedBy *ExtDTrxOp `json:"created_by"` 85 | CanceledBy *ExtDTrxOp `json:"canceled_by"` 86 | ExecutionIrreversible bool `json:"execution_irreversible"` 87 | DTrxCreationIrreversible bool `json:"dtrx_creation_irreversible"` 88 | DTrxCancelationIrreversible bool `json:"dtrx_cancelation_irreversible"` 89 | } 90 | 91 | type ActionRef struct { 92 | BlockID string `json:"block_id"` 93 | TrxIndex int `json:"trx_index"` 94 | TrxID string `json:"trx_id"` 95 | ActionIndex int `json:"action_index"` 96 | } 97 | 98 | type CreationFlatTree = []CreationFlatNode 99 | type CreationFlatNode = [3]int 100 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | type CommonIn struct { 10 | Type string `json:"type"` 11 | ReqID string `json:"req_id"` 12 | } 13 | 14 | type MsgIn struct { 15 | CommonIn 16 | Data json.RawMessage `json:"data"` 17 | } 18 | 19 | type CommonOut struct { 20 | Type string `json:"type"` 21 | ReqID string `json:"req_id,omitempty"` 22 | Fetch bool `json:"fetch,omitempty"` 23 | Listen bool `json:"listen,omitempty"` 24 | StartBlock int64 `json:"start_block,omitempty"` 25 | IrreversibleOnly bool `json:"irreversible_only"` 26 | WithProgress int64 `json:"with_progress,omitempty"` 27 | } 28 | 29 | func (c *CommonOut) SetType(v string) { c.Type = v } 30 | func (c *CommonOut) SetReqID(v string) { c.ReqID = v } 31 | 32 | func setType(msg OutgoingMessager) { 33 | objType := reflect.TypeOf(msg).Elem() 34 | typeName := OutgoingStructMap[objType] 35 | if typeName == "" { 36 | panic(fmt.Sprintf("invalid or unregistered message type: %T", msg)) 37 | } 38 | msg.SetType(typeName) 39 | } 40 | 41 | type MsgOut struct { 42 | CommonOut 43 | Data interface{} 44 | } 45 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | var runEndToEndTests = os.Getenv("DFUSE_API_TOKEN") != "" 14 | 15 | func Test_GetActions(t *testing.T) { 16 | if !runEndToEndTests { 17 | t.Skip("skipping end to end tests as the EOSWS_API_KEY is not set") 18 | } 19 | 20 | client := newClient(t) 21 | 22 | ga := &GetActionTraces{} 23 | ga.ReqID = "1" 24 | ga.StartBlock = -10 25 | ga.Listen = true 26 | ga.Data.Accounts = "eosio.token" 27 | ga.Data.ActionNames = "transfer" 28 | ga.Data.WithInlineTraces = true 29 | 30 | client.Send(ga) 31 | defer client.conn.Close() 32 | expectMessage(t, client, &Listening{}, nil) 33 | expectMessage(t, client, &ActionTrace{}, nil) 34 | } 35 | 36 | func Test_GetTableRowsFetch(t *testing.T) { 37 | if !runEndToEndTests { 38 | t.Skip("skipping end to end tests as the EOSWS_API_KEY is not set") 39 | } 40 | 41 | client := newClient(t) 42 | 43 | ga := &GetTableRows{} 44 | ga.ReqID = "1" 45 | ga.StartBlock = -6000 46 | ga.Listen = false 47 | ga.Fetch = true 48 | ga.WithProgress = 5 49 | ga.Data.JSON = true 50 | ga.Data.Code = "eosio.token" 51 | ga.Data.Scope = "eosio" 52 | ga.Data.Table = "accounts" 53 | client.Send(ga) 54 | defer client.conn.Close() 55 | 56 | expectMessage(t, client, &TableSnapshot{}, nil) 57 | 58 | } 59 | 60 | func Test_GetTableRowsListen(t *testing.T) { 61 | if !runEndToEndTests { 62 | t.Skip("skipping end to end tests as the EOSWS_API_KEY is not set") 63 | } 64 | 65 | client := newClient(t) 66 | 67 | ga := &GetTableRows{} 68 | ga.ReqID = "1" 69 | ga.StartBlock = -3600 70 | ga.Listen = true 71 | ga.Fetch = false 72 | ga.WithProgress = 5 73 | ga.Data.JSON = true 74 | ga.Data.Code = "eosio" 75 | ga.Data.Scope = "eosio" 76 | ga.Data.Table = "global" 77 | client.Send(ga) 78 | defer client.conn.Close() 79 | 80 | expectMessage(t, client, &Listening{}, nil) 81 | expectMessage(t, client, &TableDelta{}, func(t *testing.T, msg interface{}) { 82 | delta := msg.(*TableDelta) 83 | assert.Equal(t, "1", delta.ReqID) 84 | assert.NotEqual(t, "", delta.Data.Step) 85 | }) 86 | 87 | } 88 | 89 | func newClient(t *testing.T) *Client { 90 | t.Helper() 91 | 92 | key := os.Getenv("DFUSE_API_TOKEN") 93 | require.NotEqual(t, "", key) 94 | 95 | client, err := New("wss://staging-mainnet.eos.dfuse.io/v1/stream", key, "https://origin.example.com") 96 | require.NoError(t, err) 97 | 98 | return client 99 | } 100 | 101 | func expectMessage(t *testing.T, client *Client, messageType interface{}, validation func(t *testing.T, msg interface{})) { 102 | msg, err := client.Read() 103 | if err != nil { 104 | fmt.Println("DIED", err) 105 | t.Fail() 106 | return 107 | } 108 | 109 | msgType := reflect.TypeOf(msg).String() 110 | require.Equal(t, reflect.TypeOf(messageType).String(), msgType) 111 | 112 | if validation != nil { 113 | validation(t, msg) 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /name.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import "strings" 4 | 5 | func StringToName(s string) (val uint64, err error) { 6 | // ported from the eosio codebase, libraries/chain/include/eosio/chain/name.hpp 7 | var i uint32 8 | sLen := uint32(len(s)) 9 | for ; i <= 12; i++ { 10 | var c uint64 11 | if i < sLen { 12 | c = uint64(charToSymbol(s[i])) 13 | } 14 | 15 | if i < 12 { 16 | c &= 0x1f 17 | c <<= 64 - 5*(i+1) 18 | } else { 19 | c &= 0x0f 20 | } 21 | 22 | val |= c 23 | } 24 | 25 | return 26 | } 27 | 28 | func charToSymbol(c byte) byte { 29 | if c >= 'a' && c <= 'z' { 30 | return c - 'a' + 6 31 | } 32 | if c >= '1' && c <= '5' { 33 | return c - '1' + 1 34 | } 35 | return 0 36 | } 37 | 38 | var base32Alphabet = []byte(".12345abcdefghijklmnopqrstuvwxyz") 39 | 40 | func NameToString(in uint64) string { 41 | // ported from libraries/chain/name.cpp in eosio 42 | a := []byte{'.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.'} 43 | 44 | tmp := in 45 | i := uint32(0) 46 | for ; i <= 12; i++ { 47 | bit := 0x1f 48 | if i == 0 { 49 | bit = 0x0f 50 | } 51 | c := base32Alphabet[tmp&uint64(bit)] 52 | a[12-i] = c 53 | 54 | shift := uint(5) 55 | if i == 0 { 56 | shift = 4 57 | } 58 | 59 | tmp >>= shift 60 | } 61 | 62 | return strings.TrimRight(string(a), ".") 63 | } 64 | -------------------------------------------------------------------------------- /progress.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | func init() { 4 | RegisterIncomingMessage("progress", Progress{}) 5 | } 6 | 7 | type Progress struct { 8 | CommonIn 9 | Data struct { 10 | BlockNum uint32 `json:"block_num"` 11 | BlockID string `json:"block_id"` 12 | } `json:"data"` 13 | } 14 | -------------------------------------------------------------------------------- /registry.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import "reflect" 4 | 5 | var IncomingMessageMap = map[string]reflect.Type{} 6 | var IncomingStructMap = map[reflect.Type]string{} 7 | var OutgoingMessageMap = map[string]reflect.Type{} 8 | var OutgoingStructMap = map[reflect.Type]string{} 9 | 10 | func RegisterIncomingMessage(typeName string, obj interface{}) { 11 | refType := reflect.TypeOf(obj) 12 | IncomingMessageMap[typeName] = refType 13 | IncomingStructMap[refType] = typeName 14 | } 15 | 16 | func RegisterOutgoingMessage(typeName string, obj interface{}) { 17 | refType := reflect.TypeOf(obj) 18 | OutgoingMessageMap[typeName] = refType 19 | OutgoingStructMap[refType] = typeName 20 | } 21 | 22 | type OutgoingMessager interface { 23 | SetType(v string) 24 | SetReqID(v string) 25 | } 26 | 27 | // type IncomingMessager interface { 28 | // GetType() string 29 | // GetReqID() string 30 | // GetCommon() CommonIn 31 | // } 32 | -------------------------------------------------------------------------------- /tables.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | v1 "github.com/dfuse-io/eosws-go/mdl/v1" 7 | ) 8 | 9 | func init() { 10 | RegisterOutgoingMessage("get_table_rows", GetTableRows{}) 11 | RegisterIncomingMessage("table_delta", TableDelta{}) 12 | RegisterIncomingMessage("table_snapshot", TableSnapshot{}) 13 | } 14 | 15 | type GetTableRows struct { 16 | CommonOut 17 | 18 | Data struct { 19 | JSON bool `json:"json,omitempty"` 20 | Verbose bool `json:"verbose,omitempty"` 21 | 22 | Code string `json:"code"` 23 | Scope string `json:"scope"` 24 | Table string `json:"table"` 25 | } `json:"data"` 26 | } 27 | 28 | type TableDelta struct { 29 | CommonIn 30 | 31 | Data struct { 32 | BlockNum uint32 `json:"block_num"` 33 | DBOp *v1.DBOp `json:"dbop"` 34 | Step string `json:"step"` 35 | } `json:"data"` 36 | } 37 | 38 | type TableSnapshot struct { 39 | CommonIn 40 | 41 | Data struct { 42 | BlockNum uint32 `json:"block_num"` 43 | Rows []json.RawMessage `json:"rows"` 44 | } `json:"data"` 45 | } 46 | 47 | type TableSnapshotRow struct { 48 | Key string `json:"key"` 49 | Data json.RawMessage `json:"data"` 50 | } 51 | -------------------------------------------------------------------------------- /unlisten.go: -------------------------------------------------------------------------------- 1 | package eosws 2 | 3 | func init() { 4 | RegisterOutgoingMessage("unlisten", Unlisten{}) 5 | RegisterIncomingMessage("unlistened", Unlistened{}) 6 | } 7 | 8 | type Unlisten struct { 9 | CommonOut 10 | Data struct { 11 | ReqID string `json:"req_id"` 12 | } `json:"data"` 13 | } 14 | 15 | type Unlistened struct { 16 | CommonIn 17 | Data struct { 18 | Success bool `json:"success"` 19 | } `json:"data"` 20 | } 21 | --------------------------------------------------------------------------------