├── examples ├── upvote │ ├── .gitignore │ ├── Makefile │ ├── README.md │ └── main.go └── voting_monitor │ ├── .gitignore │ ├── README.md │ └── main.go ├── transports └── websocket │ ├── errors.go │ ├── events.go │ ├── object_stream.go │ └── transport.go ├── interfaces ├── callcloser.go └── caller.go ├── encoding ├── transaction │ ├── encoding.go │ ├── encoder_rolling.go │ └── encoder.go └── wif │ ├── decode_test.go │ ├── data_test.go │ └── decode.go ├── types ├── operation_custom_json_follow.go ├── id.go ├── int_deprecated.go ├── slice.go ├── time.go ├── operations_test.go ├── map.go ├── transaction_test.go ├── transaction.go ├── operation_object.go ├── operation_custom_json.go ├── int.go ├── uint.go ├── optype.go ├── operation.go └── operations.go ├── apis ├── login │ ├── README.md │ └── api.go ├── follow │ ├── README.md │ ├── data.go │ └── api.go ├── networkbroadcast │ ├── README.md │ └── api.go └── database │ ├── README.md │ ├── data.go │ └── api.go ├── transactions ├── chains.go ├── signing.h ├── transactions.go ├── signed_transaction_test.go ├── signing.c └── signed_transaction.go ├── .gitignore ├── internal ├── call │ └── utils.go └── rpc │ └── rpc.go ├── LICENSE ├── client.go └── README.md /examples/upvote/.gitignore: -------------------------------------------------------------------------------- 1 | upvote 2 | -------------------------------------------------------------------------------- /examples/voting_monitor/.gitignore: -------------------------------------------------------------------------------- 1 | voting_monitor 2 | -------------------------------------------------------------------------------- /examples/upvote/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | 3 | build: 4 | go build -ldflags='-r /usr/local/lib' 5 | -------------------------------------------------------------------------------- /transports/websocket/errors.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import "errors" 4 | 5 | var ErrClosing = errors.New("closing") 6 | -------------------------------------------------------------------------------- /interfaces/callcloser.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "io" 4 | 5 | type CallCloser interface { 6 | Caller 7 | io.Closer 8 | } 9 | -------------------------------------------------------------------------------- /interfaces/caller.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | type Caller interface { 4 | Call(method string, params, response interface{}) error 5 | } 6 | -------------------------------------------------------------------------------- /encoding/transaction/encoding.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | type TransactionMarshaller interface { 4 | MarshalTransaction(*Encoder) error 5 | } 6 | -------------------------------------------------------------------------------- /types/operation_custom_json_follow.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type FollowOperation struct { 4 | Follower string `json:"follower"` 5 | Following string `json:"following"` 6 | What []string `json:"what"` 7 | } 8 | -------------------------------------------------------------------------------- /apis/login/README.md: -------------------------------------------------------------------------------- 1 | # Login API 2 | 3 | This package adds support for `login_api`. 4 | 5 | ## State 6 | 7 | | Method Name | Raw Version | Full Version | 8 | | ----------------- |:-----------:|:------------:| 9 | | `login` | DONE | DONE | 10 | | `get_api_by_name` | DONE | DONE | 11 | -------------------------------------------------------------------------------- /transactions/chains.go: -------------------------------------------------------------------------------- 1 | package transactions 2 | 3 | type Chain struct { 4 | ID string 5 | } 6 | 7 | var SteemChain = &Chain{ 8 | ID: "0000000000000000000000000000000000000000000000000000000000000000", 9 | } 10 | 11 | var TestChain = &Chain{ 12 | ID: "18dcf0a285365fc58b71f18b3d3fec954aa0c141c44e4e5cb4cf777b9eab274e", 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.sh 27 | -------------------------------------------------------------------------------- /examples/upvote/README.md: -------------------------------------------------------------------------------- 1 | # Upvote 2 | 3 | Simply upvote whatever you like. 4 | 5 | ## Usage 6 | 7 | To vote for a post made by `somebody`, post permlink `somepermlink` as user `me`, execute: 8 | 9 | ```bash 10 | $ ./upvote somebody somepermlink me 11 | ``` 12 | 13 | For example 14 | 15 | ```bash 16 | ./upvote hr1 our-journey-ends-dream-coming-true-and-the-day-when-everything-went-as-planned me 17 | ``` 18 | -------------------------------------------------------------------------------- /internal/call/utils.go: -------------------------------------------------------------------------------- 1 | package call 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | 7 | // Vendor 8 | "github.com/go-steem/rpc/interfaces" 9 | ) 10 | 11 | var EmptyParams = []string{} 12 | 13 | func Raw(caller interfaces.Caller, method string, params interface{}) (*json.RawMessage, error) { 14 | var resp json.RawMessage 15 | if err := caller.Call(method, params, &resp); err != nil { 16 | return nil, err 17 | } 18 | return &resp, nil 19 | } 20 | -------------------------------------------------------------------------------- /encoding/wif/decode_test.go: -------------------------------------------------------------------------------- 1 | package wif 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/hex" 6 | "testing" 7 | ) 8 | 9 | func TestDecode(t *testing.T) { 10 | for _, d := range data { 11 | privKey, err := Decode(d.WIF) 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | 16 | expected := d.PrivateKeyHex 17 | got := hex.EncodeToString(privKey) 18 | 19 | if got != expected { 20 | t.Errorf("expected %v, got %v", expected, got) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /transactions/signing.h: -------------------------------------------------------------------------------- 1 | #ifndef GOSTEEMRPC_SIGNING_H 2 | #define GOSTEEMRPC_SIGNING_H 3 | 4 | int sign_transaction( 5 | const unsigned char *digest, 6 | const unsigned char *privkey, 7 | unsigned char *signature, 8 | int *recid 9 | ); 10 | 11 | // pubkey is expected to be 33 bytes long so that a compressed public key fits. 12 | int verify_recoverable_signature( 13 | const unsigned char *digest, 14 | const unsigned char *signature, 15 | int recid, 16 | unsigned char *pubkey 17 | ); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /types/id.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | type ID struct { 9 | ValueInt *Int 10 | ValueString string 11 | } 12 | 13 | var dot = []byte{'.'} 14 | 15 | func (id *ID) UnmarshalJSON(data []byte) error { 16 | if bytes.Contains(data, dot) { 17 | id.ValueString = string(data) 18 | return nil 19 | } 20 | 21 | var value Int 22 | if err := json.Unmarshal(data, &value); err != nil { 23 | return err 24 | } 25 | id.ValueInt = &value 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /encoding/wif/data_test.go: -------------------------------------------------------------------------------- 1 | package wif 2 | 3 | type testData struct { 4 | WIF string 5 | PrivateKeyHex string 6 | } 7 | 8 | var data = []testData{ 9 | { 10 | WIF: "5JWHY5DxTF6qN5grTtChDCYBmWHfY9zaSsw4CxEKN5eZpH9iBma", 11 | PrivateKeyHex: "5ad2b8df2c255d4a2996ee7d065e013e1bbb35c075ee6e5208aca44adc9a9d4c", 12 | }, 13 | { 14 | WIF: "5KPipdRzoxrp6dDqsBfMD6oFZG356trVHV5QBGx3rABs1zzWWs8", 15 | PrivateKeyHex: "cf9d6121ed458f24ea456ad7ff700da39e86688988cfe5c6ed6558642cf1e32f", 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /apis/follow/README.md: -------------------------------------------------------------------------------- 1 | # Follow API 2 | 3 | This package adds support for `follow_api`. 4 | 5 | ## State 6 | 7 | | Method Name | Raw Version | Full Version | 8 | | ------------------------- |:-----------:|:------------:| 9 | | `get_followers` | DONE | DONE | 10 | | `get_following` | DONE | DONE | 11 | | `get_feed_entries` | DONE | DONE | 12 | | `get_feed` | DONE | | 13 | | `get_account_reputations` | DONE | | 14 | -------------------------------------------------------------------------------- /types/int_deprecated.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "math/big" 6 | ) 7 | 8 | type Int struct { 9 | *big.Int 10 | } 11 | 12 | func (num *Int) UnmarshalJSON(data []byte) error { 13 | if data[0] == '"' { 14 | data = data[1:] 15 | data = data[:len(data)-1] 16 | var value big.Int 17 | if err := json.Unmarshal(data, &value); err != nil { 18 | return err 19 | } 20 | num.Int = &value 21 | return nil 22 | } 23 | 24 | var value int64 25 | if err := json.Unmarshal(data, &value); err != nil { 26 | return err 27 | } 28 | num.Int = big.NewInt(value) 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /apis/networkbroadcast/README.md: -------------------------------------------------------------------------------- 1 | # Network Broadcast API 2 | 3 | This package adds support for `network_broadcast_api`. 4 | 5 | ## State 6 | 7 | | Method Name | Raw Version | Full Version | 8 | | ------------------------------------- |:-----------:|:------------:| 9 | | `broadcast_transaction` | SKIPPED | DONE | 10 | | `broadcast_transaction_with_callback` | | | 11 | | `broadcast_transaction_synchronous` | DONE | DONE | 12 | | `broadcast_block` | | | 13 | | `set_bcd_trigger` | | | 14 | -------------------------------------------------------------------------------- /types/slice.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type StringSlice []string 11 | 12 | func (ss *StringSlice) UnmarshalJSON(data []byte) error { 13 | if len(data) == 0 { 14 | return nil 15 | } 16 | 17 | if data[0] == '[' { 18 | var v []string 19 | if err := json.Unmarshal(data, &v); err != nil { 20 | return errors.Wrap(err, "failed to unmarshal string slice") 21 | } 22 | *ss = v 23 | } else { 24 | var v string 25 | if err := json.Unmarshal(data, &v); err != nil { 26 | return errors.Wrap(err, "failed to unmarshal string slice") 27 | } 28 | *ss = strings.Split(v, " ") 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /types/time.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "time" 6 | 7 | // RPC 8 | "github.com/go-steem/rpc/encoding/transaction" 9 | ) 10 | 11 | const Layout = `"2006-01-02T15:04:05"` 12 | 13 | type Time struct { 14 | *time.Time 15 | } 16 | 17 | func (t *Time) MarshalJSON() ([]byte, error) { 18 | return []byte(t.Time.Format(Layout)), nil 19 | } 20 | 21 | func (t *Time) UnmarshalJSON(data []byte) error { 22 | parsed, err := time.ParseInLocation(Layout, string(data), time.UTC) 23 | if err != nil { 24 | return err 25 | } 26 | t.Time = &parsed 27 | return nil 28 | } 29 | 30 | func (t *Time) MarshalTransaction(encoder *transaction.Encoder) error { 31 | return encoder.Encode(uint32(t.Time.Unix())) 32 | } 33 | -------------------------------------------------------------------------------- /apis/follow/data.go: -------------------------------------------------------------------------------- 1 | package follow 2 | 3 | const ( 4 | FollowKindFollow = "blog" 5 | FollowKindIgnore = "ignore" 6 | ) 7 | 8 | type FollowObject struct { 9 | Follower string `json:"follower"` 10 | Following string `json:"following"` 11 | What []string `json:"what"` 12 | } 13 | 14 | type FeedEntry struct { 15 | Author string `json:"string"` 16 | Permlink string `json:"permlink"` 17 | EntryID uint32 `json:"entry_id"` 18 | } 19 | 20 | /* 21 | type CommentFeedEntry struct { 22 | Comment *CommentObject `json:"comment"` 23 | EntryID uint32 `json:"entry_id"` 24 | } 25 | */ 26 | 27 | /* 28 | type AccountReputation struct { 29 | Account string `json:"account"` 30 | Reputation ??? `json:"reputation"` 31 | } 32 | */ 33 | -------------------------------------------------------------------------------- /internal/rpc/rpc.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | 7 | // RPC 8 | "github.com/go-steem/rpc/interfaces" 9 | 10 | // Vendor 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | func GetNumericAPIID(caller interfaces.Caller, apiName string) (int, error) { 15 | params := []interface{}{apiName} 16 | 17 | var resp json.RawMessage 18 | if err := caller.Call("call", []interface{}{1, "get_api_by_name", params}, &resp); err != nil { 19 | return 0, err 20 | } 21 | 22 | if string(resp) == "null" { 23 | return 0, errors.Errorf("API not available: %v", apiName) 24 | } 25 | 26 | var id int 27 | if err := json.Unmarshal([]byte(resp), &id); err != nil { 28 | return 0, err 29 | } 30 | return id, nil 31 | } 32 | -------------------------------------------------------------------------------- /encoding/wif/decode.go: -------------------------------------------------------------------------------- 1 | package wif 2 | 3 | import ( 4 | // Vendor 5 | "github.com/btcsuite/btcutil" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // Decode can be used to turn WIF into a raw private key (32 bytes). 10 | func Decode(wif string) ([]byte, error) { 11 | w, err := btcutil.DecodeWIF(wif) 12 | if err != nil { 13 | return nil, errors.Wrap(err, "failed to decode WIF") 14 | } 15 | 16 | return w.PrivKey.Serialize(), nil 17 | } 18 | 19 | // GetPublicKey returns the public key associated with the given WIF 20 | // in the 33-byte compressed format. 21 | func GetPublicKey(wif string) ([]byte, error) { 22 | w, err := btcutil.DecodeWIF(wif) 23 | if err != nil { 24 | return nil, errors.Wrap(err, "failed to decode WIF") 25 | } 26 | 27 | return w.PrivKey.PubKey().SerializeCompressed(), nil 28 | } 29 | -------------------------------------------------------------------------------- /types/operations_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "bytes" 6 | "encoding/hex" 7 | "testing" 8 | 9 | // RPC 10 | "github.com/go-steem/rpc/encoding/transaction" 11 | ) 12 | 13 | func TestVoteOperation_MarshalTransaction(t *testing.T) { 14 | op := &VoteOperation{ 15 | Voter: "xeroc", 16 | Author: "xeroc", 17 | Permlink: "piston", 18 | Weight: 10000, 19 | } 20 | 21 | expectedHex := "00057865726f63057865726f6306706973746f6e1027" 22 | 23 | var b bytes.Buffer 24 | encoder := transaction.NewEncoder(&b) 25 | 26 | if err := encoder.Encode(op); err != nil { 27 | t.Error(err) 28 | } 29 | 30 | serializedHex := hex.EncodeToString(b.Bytes()) 31 | 32 | if serializedHex != expectedHex { 33 | t.Errorf("expected %v, got %v", expectedHex, serializedHex) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/voting_monitor/README.md: -------------------------------------------------------------------------------- 1 | # Voting Monitor 2 | 3 | In this example we connect to `steemd` and watch operations as they 4 | are happening. Every time we see a `vote` operation, we print a message 5 | into the console. 6 | 7 | ``` 8 | $ ./monitor_voting -rpc_endpoint="ws://$(docker-machine ip default):8090" 9 | 2016/05/29 10:42:56 ---> Dial("ws://192.168.99.100:8090") 10 | 2016/05/29 10:42:56 ---> GetConfig() 11 | 2016/05/29 10:42:56 ---> Entering the block processing loop (last block = 1866869) 12 | @easteagle13 voted for @easteagle13/another-article-discussing-some-inherent-flaws-of-the-dao 13 | @easteagle13 voted for @easteagle13/to-your-loss-of-a-friend-my-condolences-and-other-thoughts 14 | @yefet voted for @alexgr/planning-for-long-term-success-of-steemit-identifying-areas-of-improvement 15 | @dke voted for @steemrollin/steem-meme 16 | ... 17 | ``` 18 | -------------------------------------------------------------------------------- /transports/websocket/events.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // ConnectingEvent is emitted when a new connection is being established. 8 | type ConnectingEvent struct { 9 | URL string 10 | } 11 | 12 | func (e *ConnectingEvent) String() string { 13 | return fmt.Sprintf("CONNECTING [url=%v]", e.URL) 14 | } 15 | 16 | // ConnectedEvent is emitted when the WebSocket connection is established. 17 | type ConnectedEvent struct { 18 | URL string 19 | } 20 | 21 | func (e *ConnectedEvent) String() string { 22 | return fmt.Sprintf("CONNECTED [url=%v]", e.URL) 23 | } 24 | 25 | // DisconnectedEvent is emitted when the WebSocket connection is lost. 26 | type DisconnectedEvent struct { 27 | URL string 28 | Err error 29 | } 30 | 31 | func (e *DisconnectedEvent) String() string { 32 | return fmt.Sprintf("DISCONNECTED [url=%v, err=%v]", e.URL, e.Err) 33 | } 34 | -------------------------------------------------------------------------------- /encoding/transaction/encoder_rolling.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | type RollingEncoder struct { 4 | next *Encoder 5 | err error 6 | } 7 | 8 | func NewRollingEncoder(next *Encoder) *RollingEncoder { 9 | return &RollingEncoder{next, nil} 10 | } 11 | 12 | func (encoder *RollingEncoder) EncodeVarint(i int64) { 13 | if encoder.err == nil { 14 | encoder.err = encoder.next.EncodeVarint(i) 15 | } 16 | } 17 | 18 | func (encoder *RollingEncoder) EncodeUVarint(i uint64) { 19 | if encoder.err == nil { 20 | encoder.err = encoder.next.EncodeUVarint(i) 21 | } 22 | } 23 | 24 | func (encoder *RollingEncoder) EncodeNumber(v interface{}) { 25 | if encoder.err == nil { 26 | encoder.err = encoder.next.EncodeNumber(v) 27 | } 28 | } 29 | 30 | func (encoder *RollingEncoder) Encode(v interface{}) { 31 | if encoder.err == nil { 32 | encoder.err = encoder.next.Encode(v) 33 | } 34 | } 35 | 36 | func (encoder *RollingEncoder) Err() error { 37 | return encoder.err 38 | } 39 | -------------------------------------------------------------------------------- /types/map.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | type StringInt64Map map[string]int64 9 | 10 | func (m StringInt64Map) MarshalJSON() ([]byte, error) { 11 | xs := make([]interface{}, len(m)) 12 | for k, v := range m { 13 | xs = append(xs, []interface{}{k, v}) 14 | } 15 | return json.Marshal(xs) 16 | } 17 | 18 | func (m *StringInt64Map) UnmarshalJSON(data []byte) error { 19 | var xs [][]interface{} 20 | if err := json.Unmarshal(data, &xs); err != nil { 21 | return err 22 | } 23 | 24 | var invalid bool 25 | mp := make(map[string]int64, len(xs)) 26 | for _, kv := range xs { 27 | if len(kv) != 2 { 28 | invalid = true 29 | break 30 | } 31 | 32 | k, ok := kv[0].(string) 33 | if !ok { 34 | invalid = true 35 | break 36 | } 37 | 38 | var v int64 39 | switch t := kv[1].(type) { 40 | case float64: 41 | v = int64(t) 42 | case int64: 43 | v = t 44 | default: 45 | invalid = true 46 | break 47 | } 48 | 49 | mp[k] = v 50 | } 51 | if invalid { 52 | return errors.New("invalid map encoding") 53 | } 54 | 55 | *m = mp 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 go-steem 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 | -------------------------------------------------------------------------------- /types/transaction_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "bytes" 6 | "encoding/hex" 7 | "testing" 8 | "time" 9 | 10 | // RPC 11 | "github.com/go-steem/rpc/encoding/transaction" 12 | ) 13 | 14 | func TestTransaction_MarshalTransaction(t *testing.T) { 15 | // The result we expect. 16 | expected := "bd8c5fe26f45f179a8570100057865726f63057865726f6306706973746f6e102700" 17 | 18 | // Prepare the transaction. 19 | expiration := time.Date(2016, 8, 8, 12, 24, 17, 0, time.UTC) 20 | tx := Transaction{ 21 | RefBlockNum: 36029, 22 | RefBlockPrefix: 1164960351, 23 | Expiration: &Time{&expiration}, 24 | } 25 | tx.PushOperation(&VoteOperation{ 26 | Voter: "xeroc", 27 | Author: "xeroc", 28 | Permlink: "piston", 29 | Weight: 10000, 30 | }) 31 | 32 | // Marshal the transaction. 33 | var b bytes.Buffer 34 | encoder := transaction.NewEncoder(&b) 35 | 36 | if err := tx.MarshalTransaction(encoder); err != nil { 37 | t.Error(err) 38 | } 39 | got := hex.EncodeToString(b.Bytes()) 40 | 41 | // Compare that we got with what we expect to get. 42 | if got != expected { 43 | t.Errorf("expected %v, got %v", expected, got) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /transports/websocket/object_stream.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gorilla/websocket" 7 | jsonrpc2websocket "github.com/sourcegraph/jsonrpc2/websocket" 8 | ) 9 | 10 | // ObjectStream implements jsonrpc2.ObjectStream that uses a WebSocket. 11 | // It extends jsonrpc2/websocket.ObjectStream with read/write timeouts. 12 | type ObjectStream struct { 13 | conn *websocket.Conn 14 | stream jsonrpc2websocket.ObjectStream 15 | 16 | writeTimeout time.Duration 17 | readTimeout time.Duration 18 | } 19 | 20 | func NewObjectStream(conn *websocket.Conn, writeTimeout, readTimeout time.Duration) *ObjectStream { 21 | return &ObjectStream{conn, jsonrpc2websocket.NewObjectStream(conn), writeTimeout, readTimeout} 22 | } 23 | 24 | func (stream *ObjectStream) WriteObject(v interface{}) error { 25 | stream.conn.SetWriteDeadline(time.Now().Add(stream.writeTimeout)) 26 | return stream.stream.WriteObject(v) 27 | } 28 | 29 | func (stream *ObjectStream) ReadObject(v interface{}) error { 30 | stream.conn.SetReadDeadline(time.Now().Add(stream.readTimeout)) 31 | return stream.stream.ReadObject(v) 32 | } 33 | 34 | func (stream *ObjectStream) Close() error { 35 | return stream.stream.Close() 36 | } 37 | -------------------------------------------------------------------------------- /transactions/transactions.go: -------------------------------------------------------------------------------- 1 | package transactions 2 | 3 | import ( 4 | // Stdlib 5 | "bytes" 6 | "encoding/binary" 7 | "encoding/hex" 8 | 9 | // RPC 10 | "github.com/go-steem/rpc/types" 11 | 12 | // Vendor 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func RefBlockNum(blockNumber types.UInt32) types.UInt16 { 17 | return types.UInt16(blockNumber) 18 | } 19 | 20 | func RefBlockPrefix(blockID string) (types.UInt32, error) { 21 | // Block ID is hex-encoded. 22 | rawBlockID, err := hex.DecodeString(blockID) 23 | if err != nil { 24 | return 0, errors.Wrapf(err, "networkbroadcast: failed to decode block ID: %v", blockID) 25 | } 26 | 27 | // Raw prefix = raw block ID [4:8]. 28 | // Make sure we don't trigger a slice bounds out of range panic. 29 | if len(rawBlockID) < 8 { 30 | return 0, errors.Errorf("networkbroadcast: invalid block ID: %v", blockID) 31 | } 32 | rawPrefix := rawBlockID[4:8] 33 | 34 | // Decode the prefix. 35 | var prefix uint32 36 | if err := binary.Read(bytes.NewReader(rawPrefix), binary.LittleEndian, &prefix); err != nil { 37 | return 0, errors.Wrapf(err, "networkbroadcast: failed to read block prefix: %v", rawPrefix) 38 | } 39 | 40 | // Done, return the prefix. 41 | return types.UInt32(prefix), nil 42 | } 43 | -------------------------------------------------------------------------------- /types/transaction.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // RPC 5 | "github.com/go-steem/rpc/encoding/transaction" 6 | 7 | // Vendor 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Transaction represents a blockchain transaction. 12 | type Transaction struct { 13 | RefBlockNum UInt16 `json:"ref_block_num"` 14 | RefBlockPrefix UInt32 `json:"ref_block_prefix"` 15 | Expiration *Time `json:"expiration"` 16 | Operations Operations `json:"operations"` 17 | Signatures []string `json:"signatures"` 18 | } 19 | 20 | // MarshalTransaction implements transaction.Marshaller interface. 21 | func (tx *Transaction) MarshalTransaction(encoder *transaction.Encoder) error { 22 | if len(tx.Operations) == 0 { 23 | return errors.New("no operation specified") 24 | } 25 | 26 | enc := transaction.NewRollingEncoder(encoder) 27 | 28 | enc.Encode(tx.RefBlockNum) 29 | enc.Encode(tx.RefBlockPrefix) 30 | enc.Encode(tx.Expiration) 31 | 32 | enc.EncodeUVarint(uint64(len(tx.Operations))) 33 | for _, op := range tx.Operations { 34 | enc.Encode(op) 35 | } 36 | 37 | // Extensions are not supported yet. 38 | enc.EncodeUVarint(0) 39 | 40 | return enc.Err() 41 | } 42 | 43 | // PushOperation can be used to add an operation into the transaction. 44 | func (tx *Transaction) PushOperation(op Operation) { 45 | tx.Operations = append(tx.Operations, op) 46 | } 47 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | // RPC 5 | "github.com/go-steem/rpc/apis/database" 6 | "github.com/go-steem/rpc/apis/follow" 7 | "github.com/go-steem/rpc/apis/login" 8 | "github.com/go-steem/rpc/apis/networkbroadcast" 9 | "github.com/go-steem/rpc/interfaces" 10 | ) 11 | 12 | // Client can be used to access Steem remote APIs. 13 | // 14 | // There is a public field for every Steem API available, 15 | // e.g. Client.Database corresponds to database_api. 16 | type Client struct { 17 | cc interfaces.CallCloser 18 | 19 | // Login represents login_api. 20 | Login *login.API 21 | 22 | // Database represents database_api. 23 | Database *database.API 24 | 25 | // Follow represents follow_api. 26 | Follow *follow.API 27 | 28 | // NetworkBroadcast represents network_broadcast_api. 29 | NetworkBroadcast *networkbroadcast.API 30 | } 31 | 32 | // NewClient creates a new RPC client that use the given CallCloser internally. 33 | func NewClient(cc interfaces.CallCloser) (*Client, error) { 34 | client := &Client{cc: cc} 35 | client.Login = login.NewAPI(client.cc) 36 | client.Database = database.NewAPI(client.cc) 37 | 38 | followAPI, err := follow.NewAPI(client.cc) 39 | if err != nil { 40 | return nil, err 41 | } 42 | client.Follow = followAPI 43 | 44 | networkBroadcastAPI, err := networkbroadcast.NewAPI(client.cc) 45 | if err != nil { 46 | return nil, err 47 | } 48 | client.NetworkBroadcast = networkBroadcastAPI 49 | 50 | return client, nil 51 | } 52 | 53 | // Close should be used to close the client when no longer needed. 54 | // It simply calls Close() on the underlying CallCloser. 55 | func (client *Client) Close() error { 56 | return client.cc.Close() 57 | } 58 | -------------------------------------------------------------------------------- /apis/login/api.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | 7 | // RPC 8 | "github.com/go-steem/rpc/interfaces" 9 | 10 | // Vendor 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | const ( 15 | APIID = "login_api" 16 | NumbericAPIID = 1 17 | ) 18 | 19 | type API struct { 20 | caller interfaces.Caller 21 | } 22 | 23 | func NewAPI(caller interfaces.Caller) *API { 24 | return &API{caller} 25 | } 26 | 27 | func (api *API) call(method string, params, resp interface{}) error { 28 | return api.caller.Call("call", []interface{}{NumbericAPIID, method, params}, resp) 29 | } 30 | 31 | func (api *API) LoginRaw(username, password string) (*json.RawMessage, error) { 32 | var resp json.RawMessage 33 | params := []interface{}{username, password} 34 | if err := api.call("login", params, &resp); err != nil { 35 | return nil, errors.Wrap(err, "go-steem/rpc: login_api: failed to call login") 36 | } 37 | return &resp, nil 38 | } 39 | 40 | func (api *API) Login(username, password string) (bool, error) { 41 | var resp bool 42 | params := []interface{}{username, password} 43 | if err := api.call("login", params, &resp); err != nil { 44 | return false, errors.Wrap(err, "go-steem/rpc: login_api: failed to call login") 45 | } 46 | return resp, nil 47 | } 48 | 49 | func (api *API) GetAPIByNameRaw(apiName string) (*json.RawMessage, error) { 50 | var resp json.RawMessage 51 | params := []interface{}{apiName} 52 | if err := api.call("get_api_by_name", params, &resp); err != nil { 53 | return nil, errors.Wrap(err, "go-steem/rpc: login_api: failed to call get_api_by_name") 54 | } 55 | return &resp, nil 56 | } 57 | 58 | func (api *API) GetAPIByName(apiName string) (int, error) { 59 | var resp int 60 | params := []interface{}{apiName} 61 | if err := api.call("get_api_by_name", params, &resp); err != nil { 62 | return 0, errors.Wrap(err, "go-steem/rpc: login_api: failed to call get_api_by_name") 63 | } 64 | return resp, nil 65 | } 66 | -------------------------------------------------------------------------------- /types/operation_object.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type OperationObject struct { 8 | BlockNumber uint32 `json:"block"` 9 | TransactionID string `json:"trx_id"` 10 | TransactionInBlock uint32 `json:"trx_in_block"` 11 | Operation Operation `json:"op"` 12 | OperationInTransaction uint16 `json:"op_in_trx"` 13 | VirtualOperation uint64 `json:"virtual_op"` 14 | Timestamp *Time `json:"timestamp"` 15 | } 16 | 17 | type rawOperationObject struct { 18 | BlockNumber uint32 `json:"block"` 19 | TransactionID string `json:"trx_id"` 20 | TransactionInBlock uint32 `json:"trx_in_block"` 21 | Operation *operationTuple `json:"op"` 22 | OperationInTransaction uint16 `json:"op_in_trx"` 23 | VirtualOperation uint64 `json:"virtual_op"` 24 | Timestamp *Time `json:"timestamp"` 25 | } 26 | 27 | func (op *OperationObject) UnmarshalJSON(p []byte) error { 28 | var raw rawOperationObject 29 | if err := json.Unmarshal(p, &raw); err != nil { 30 | return err 31 | } 32 | 33 | op.BlockNumber = raw.BlockNumber 34 | op.TransactionID = raw.TransactionID 35 | op.TransactionInBlock = raw.TransactionInBlock 36 | op.Operation = raw.Operation.Data 37 | op.OperationInTransaction = raw.OperationInTransaction 38 | op.VirtualOperation = raw.VirtualOperation 39 | op.Timestamp = raw.Timestamp 40 | return nil 41 | } 42 | 43 | func (op *OperationObject) MarshalJSON() ([]byte, error) { 44 | return json.Marshal(&rawOperationObject{ 45 | BlockNumber: op.BlockNumber, 46 | TransactionID: op.TransactionID, 47 | TransactionInBlock: op.TransactionInBlock, 48 | Operation: &operationTuple{op.Operation.Type(), op.Operation}, 49 | OperationInTransaction: op.OperationInTransaction, 50 | VirtualOperation: op.VirtualOperation, 51 | Timestamp: op.Timestamp, 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /apis/networkbroadcast/api.go: -------------------------------------------------------------------------------- 1 | package networkbroadcast 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | 7 | // RPC 8 | "github.com/go-steem/rpc/interfaces" 9 | "github.com/go-steem/rpc/internal/rpc" 10 | "github.com/go-steem/rpc/types" 11 | 12 | // Vendor 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const APIID = "network_broadcast_api" 17 | 18 | type API struct { 19 | id int 20 | caller interfaces.Caller 21 | } 22 | 23 | func NewAPI(caller interfaces.Caller) (*API, error) { 24 | id, err := rpc.GetNumericAPIID(caller, APIID) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return &API{id, caller}, nil 29 | } 30 | 31 | func (api *API) call(method string, params, resp interface{}) error { 32 | return api.caller.Call("call", []interface{}{api.id, method, params}, resp) 33 | } 34 | 35 | /* 36 | * broadcast_transaction 37 | */ 38 | 39 | func (api *API) BroadcastTransaction(tx *types.Transaction) error { 40 | params := []interface{}{tx} 41 | return api.call("broadcast_transaction", params, nil) 42 | } 43 | 44 | /* 45 | * broadcast_transaction_synchronous 46 | */ 47 | 48 | func (api *API) BroadcastTransactionSynchronousRaw(tx *types.Transaction) (*json.RawMessage, error) { 49 | params := []interface{}{tx} 50 | 51 | var resp json.RawMessage 52 | if err := api.call("broadcast_transaction_synchronous", params, &resp); err != nil { 53 | return nil, err 54 | } 55 | return &resp, nil 56 | } 57 | 58 | type BroadcastResponse struct { 59 | ID string `json:"id"` 60 | BlockNum uint32 `json:"block_num"` 61 | TrxNum uint32 `json:"trx_num"` 62 | Expired bool `json:"expired"` 63 | } 64 | 65 | func (api *API) BroadcastTransactionSynchronous(tx *types.Transaction) (*BroadcastResponse, error) { 66 | raw, err := api.BroadcastTransactionSynchronousRaw(tx) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | var resp BroadcastResponse 72 | if err := json.Unmarshal([]byte(*raw), &resp); err != nil { 73 | return nil, errors.Wrapf(err, "failed to unmarshal BroadcastResponse: %v", string(*raw)) 74 | } 75 | return &resp, nil 76 | } 77 | -------------------------------------------------------------------------------- /transactions/signed_transaction_test.go: -------------------------------------------------------------------------------- 1 | package transactions 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/hex" 6 | "testing" 7 | "time" 8 | 9 | // RPC 10 | "github.com/go-steem/rpc/encoding/wif" 11 | "github.com/go-steem/rpc/types" 12 | ) 13 | 14 | var tx *types.Transaction 15 | 16 | func init() { 17 | // Prepare the transaction. 18 | expiration := time.Date(2016, 8, 8, 12, 24, 17, 0, time.UTC) 19 | tx = &types.Transaction{ 20 | RefBlockNum: 36029, 21 | RefBlockPrefix: 1164960351, 22 | Expiration: &types.Time{&expiration}, 23 | } 24 | tx.PushOperation(&types.VoteOperation{ 25 | Voter: "xeroc", 26 | Author: "xeroc", 27 | Permlink: "piston", 28 | Weight: 10000, 29 | }) 30 | } 31 | 32 | var wifs = []string{ 33 | "5JLw5dgQAx6rhZEgNN5C2ds1V47RweGshynFSWFbaMohsYsBvE8", 34 | } 35 | 36 | var privateKeys = make([][]byte, 0, len(wifs)) 37 | 38 | func init() { 39 | for _, v := range wifs { 40 | privKey, err := wif.Decode(v) 41 | if err != nil { 42 | panic(err) 43 | } 44 | privateKeys = append(privateKeys, privKey) 45 | } 46 | } 47 | 48 | var publicKeys = make([][]byte, 0, len(wifs)) 49 | 50 | func init() { 51 | for _, v := range wifs { 52 | pubKey, err := wif.GetPublicKey(v) 53 | if err != nil { 54 | panic(err) 55 | } 56 | publicKeys = append(publicKeys, pubKey) 57 | } 58 | } 59 | 60 | func TestTransaction_Digest(t *testing.T) { 61 | expected := "582176b1daf89984bc8b4fdcb24ff1433d1eb114a8c4bf20fb22ad580d035889" 62 | 63 | stx := NewSignedTransaction(tx) 64 | 65 | digest, err := stx.Digest(SteemChain) 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | 70 | got := hex.EncodeToString(digest) 71 | if got != expected { 72 | t.Errorf("got %v, expected %v", got, expected) 73 | } 74 | } 75 | 76 | func TestTransaction_SignAndVerify(t *testing.T) { 77 | tx.Signatures = nil 78 | defer func() { 79 | tx.Signatures = nil 80 | }() 81 | 82 | stx := NewSignedTransaction(tx) 83 | if err := stx.Sign(privateKeys, SteemChain); err != nil { 84 | t.Error(err) 85 | } 86 | 87 | if len(tx.Signatures) != 1 { 88 | t.Error("expected signatures not appended to the transaction") 89 | } 90 | 91 | ok, err := stx.Verify(publicKeys, SteemChain) 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | if !ok { 96 | t.Error("verification failed") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /types/operation_custom_json.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "bytes" 6 | "encoding/json" 7 | "io" 8 | "reflect" 9 | "strings" 10 | 11 | // Vendor 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | TypeFollow = "follow" 17 | ) 18 | 19 | var customJSONDataObjects = map[string]interface{}{ 20 | TypeFollow: &FollowOperation{}, 21 | } 22 | 23 | // FC_REFLECT( steemit::chain::custom_json_operation, 24 | // (required_auths) 25 | // (required_posting_auths) 26 | // (id) 27 | // (json) ) 28 | 29 | // CustomJSONOperation represents custom_json operation data. 30 | type CustomJSONOperation struct { 31 | RequiredAuths []string `json:"required_auths"` 32 | RequiredPostingAuths []string `json:"required_posting_auths"` 33 | ID string `json:"id"` 34 | JSON string `json:"json"` 35 | } 36 | 37 | func (op *CustomJSONOperation) Type() OpType { 38 | return TypeCustomJSON 39 | } 40 | 41 | func (op *CustomJSONOperation) Data() interface{} { 42 | return op 43 | } 44 | 45 | func (op *CustomJSONOperation) UnmarshalData() (interface{}, error) { 46 | // Get the corresponding data object template. 47 | template, ok := customJSONDataObjects[op.ID] 48 | if !ok { 49 | // In case there is no corresponding template, return nil. 50 | return nil, nil 51 | } 52 | 53 | // Clone the template. 54 | opData := reflect.New(reflect.Indirect(reflect.ValueOf(template)).Type()).Interface() 55 | 56 | // Prepare the whole operation tuple. 57 | var bodyReader io.Reader 58 | if op.JSON[0] == '[' { 59 | rawTuple := make([]json.RawMessage, 2) 60 | if err := json.NewDecoder(strings.NewReader(op.JSON)).Decode(&rawTuple); err != nil { 61 | return nil, errors.Wrapf(err, 62 | "failed to unmarshal CustomJSONOperation.JSON: \n%v", op.JSON) 63 | } 64 | if rawTuple[1] == nil { 65 | return nil, errors.Errorf("invalid CustomJSONOperation.JSON: \n%v", op.JSON) 66 | } 67 | bodyReader = bytes.NewReader([]byte(rawTuple[1])) 68 | } else { 69 | bodyReader = strings.NewReader(op.JSON) 70 | } 71 | 72 | // Unmarshal into the new object instance. 73 | if err := json.NewDecoder(bodyReader).Decode(opData); err != nil { 74 | return nil, errors.Wrapf(err, 75 | "failed to unmarshal CustomJSONOperation.JSON: \n%v", op.JSON) 76 | } 77 | 78 | return opData, nil 79 | } 80 | -------------------------------------------------------------------------------- /types/int.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | "strconv" 7 | 8 | // RPC 9 | "github.com/go-steem/rpc/encoding/transaction" 10 | 11 | // Vendor 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func unmarshalInt(data []byte) (int64, error) { 16 | if len(data) == 0 { 17 | return 0, errors.New("types: empty data received when unmarshalling an integer") 18 | } 19 | 20 | var ( 21 | i int64 22 | err error 23 | ) 24 | if data[0] == '"' { 25 | d := data[1:] 26 | d = d[:len(d)-1] 27 | i, err = strconv.ParseInt(string(d), 10, 64) 28 | } else { 29 | err = json.Unmarshal(data, &i) 30 | } 31 | return i, errors.Wrapf(err, "types: failed to unmarshal integer: %v", data) 32 | } 33 | 34 | /* 35 | type Int int 36 | 37 | func (num *Int) UnmarshalJSON(data []byte) error { 38 | v, err := unmarshalInt(data) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | *num = Int(v) 44 | return nil 45 | } 46 | */ 47 | 48 | type Int8 int8 49 | 50 | func (num *Int8) UnmarshalJSON(data []byte) error { 51 | v, err := unmarshalInt(data) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | *num = Int8(v) 57 | return nil 58 | } 59 | 60 | func (num Int8) MarshalTransaction(encoder *transaction.Encoder) error { 61 | return encoder.EncodeNumber(int(num)) 62 | } 63 | 64 | type Int16 int16 65 | 66 | func (num *Int16) UnmarshalJSON(data []byte) error { 67 | v, err := unmarshalInt(data) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | *num = Int16(v) 73 | return nil 74 | } 75 | 76 | func (num Int16) MarshalTransaction(encoder *transaction.Encoder) error { 77 | return encoder.EncodeNumber(int16(num)) 78 | } 79 | 80 | type Int32 int32 81 | 82 | func (num *Int32) UnmarshalJSON(data []byte) error { 83 | v, err := unmarshalInt(data) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | *num = Int32(v) 89 | return nil 90 | } 91 | 92 | func (num Int32) MarshalTransaction(encoder *transaction.Encoder) error { 93 | return encoder.EncodeNumber(int32(num)) 94 | } 95 | 96 | type Int64 int64 97 | 98 | func (num *Int64) UnmarshalJSON(data []byte) error { 99 | v, err := unmarshalInt(data) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | *num = Int64(v) 105 | return nil 106 | } 107 | 108 | func (num Int64) MarshalTransaction(encoder *transaction.Encoder) error { 109 | return encoder.EncodeNumber(int64(num)) 110 | } 111 | -------------------------------------------------------------------------------- /types/uint.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | "strconv" 7 | 8 | // RPC 9 | "github.com/go-steem/rpc/encoding/transaction" 10 | 11 | // Vendor 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func unmarshalUInt(data []byte) (uint64, error) { 16 | if len(data) == 0 { 17 | return 0, errors.New("types: empty data received when unmarshalling an unsigned integer") 18 | } 19 | 20 | var ( 21 | i uint64 22 | err error 23 | ) 24 | if data[0] == '"' { 25 | d := data[1:] 26 | d = d[:len(d)-1] 27 | i, err = strconv.ParseUint(string(d), 10, 64) 28 | } else { 29 | err = json.Unmarshal(data, &i) 30 | } 31 | return i, errors.Wrapf(err, "types: failed to unmarshal unsigned integer: %v", data) 32 | } 33 | 34 | type UInt uint 35 | 36 | func (num *UInt) UnmarshalJSON(data []byte) error { 37 | v, err := unmarshalUInt(data) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | *num = UInt(v) 43 | return nil 44 | } 45 | 46 | func (num UInt) MarshalTransaction(encoder *transaction.Encoder) error { 47 | return encoder.EncodeNumber(uint(num)) 48 | } 49 | 50 | type UInt8 uint8 51 | 52 | func (num *UInt8) UnmarshalJSON(data []byte) error { 53 | v, err := unmarshalUInt(data) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | *num = UInt8(v) 59 | return nil 60 | } 61 | 62 | func (num UInt8) MarshalTransaction(encoder *transaction.Encoder) error { 63 | return encoder.EncodeNumber(uint8(num)) 64 | } 65 | 66 | type UInt16 uint16 67 | 68 | func (num *UInt16) UnmarshalJSON(data []byte) error { 69 | v, err := unmarshalUInt(data) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | *num = UInt16(v) 75 | return nil 76 | } 77 | 78 | func (num UInt16) MarshalTransaction(encoder *transaction.Encoder) error { 79 | return encoder.EncodeNumber(uint16(num)) 80 | } 81 | 82 | type UInt32 uint32 83 | 84 | func (num *UInt32) UnmarshalJSON(data []byte) error { 85 | v, err := unmarshalUInt(data) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | *num = UInt32(v) 91 | return nil 92 | } 93 | 94 | func (num UInt32) MarshalTransaction(encoder *transaction.Encoder) error { 95 | return encoder.EncodeNumber(uint32(num)) 96 | } 97 | 98 | type UInt64 uint64 99 | 100 | func (num *UInt64) UnmarshalJSON(data []byte) error { 101 | v, err := unmarshalUInt(data) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | *num = UInt64(v) 107 | return nil 108 | } 109 | 110 | func (num UInt64) MarshalTransaction(encoder *transaction.Encoder) error { 111 | return encoder.EncodeNumber(uint64(num)) 112 | } 113 | -------------------------------------------------------------------------------- /encoding/transaction/encoder.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/binary" 6 | "io" 7 | "strings" 8 | 9 | // Vendor 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type Encoder struct { 14 | w io.Writer 15 | } 16 | 17 | func NewEncoder(w io.Writer) *Encoder { 18 | return &Encoder{w} 19 | } 20 | 21 | func (encoder *Encoder) EncodeVarint(i int64) error { 22 | if i >= 0 { 23 | return encoder.EncodeUVarint(uint64(i)) 24 | } 25 | 26 | b := make([]byte, binary.MaxVarintLen64) 27 | n := binary.PutVarint(b, i) 28 | return encoder.writeBytes(b[:n]) 29 | } 30 | 31 | func (encoder *Encoder) EncodeUVarint(i uint64) error { 32 | b := make([]byte, binary.MaxVarintLen64) 33 | n := binary.PutUvarint(b, i) 34 | return encoder.writeBytes(b[:n]) 35 | } 36 | 37 | func (encoder *Encoder) EncodeNumber(v interface{}) error { 38 | if err := binary.Write(encoder.w, binary.LittleEndian, v); err != nil { 39 | return errors.Wrapf(err, "encoder: failed to write number: %v", v) 40 | } 41 | return nil 42 | } 43 | 44 | func (encoder *Encoder) Encode(v interface{}) error { 45 | if marshaller, ok := v.(TransactionMarshaller); ok { 46 | return marshaller.MarshalTransaction(encoder) 47 | } 48 | 49 | switch v := v.(type) { 50 | case int: 51 | return encoder.EncodeNumber(v) 52 | case int8: 53 | return encoder.EncodeNumber(v) 54 | case int16: 55 | return encoder.EncodeNumber(v) 56 | case int32: 57 | return encoder.EncodeNumber(v) 58 | case int64: 59 | return encoder.EncodeNumber(v) 60 | 61 | case uint: 62 | return encoder.EncodeNumber(v) 63 | case uint8: 64 | return encoder.EncodeNumber(v) 65 | case uint16: 66 | return encoder.EncodeNumber(v) 67 | case uint32: 68 | return encoder.EncodeNumber(v) 69 | case uint64: 70 | return encoder.EncodeNumber(v) 71 | 72 | case string: 73 | return encoder.encodeString(v) 74 | 75 | default: 76 | return errors.Errorf("encoder: unsupported type encountered") 77 | } 78 | } 79 | 80 | func (encoder *Encoder) encodeString(v string) error { 81 | if err := encoder.EncodeUVarint(uint64(len(v))); err != nil { 82 | return errors.Wrapf(err, "encoder: failed to write string: %v", v) 83 | } 84 | 85 | return encoder.writeString(v) 86 | } 87 | 88 | func (encoder *Encoder) writeBytes(bs []byte) error { 89 | if _, err := encoder.w.Write(bs); err != nil { 90 | return errors.Wrapf(err, "encoder: failed to write bytes: %v", bs) 91 | } 92 | return nil 93 | } 94 | 95 | func (encoder *Encoder) writeString(s string) error { 96 | if _, err := io.Copy(encoder.w, strings.NewReader(s)); err != nil { 97 | return errors.Wrapf(err, "encoder: failed to write string: %v", s) 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /types/optype.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // OpType represents a Steem operation type, i.e. vote, comment, pow and so on. 4 | type OpType string 5 | 6 | // Code returns the operation code associated with the given operation type. 7 | func (kind OpType) Code() uint16 { 8 | return opCodes[kind] 9 | } 10 | 11 | const ( 12 | TypeVote OpType = "vote" 13 | TypeComment OpType = "comment" 14 | TypeTransfer OpType = "transfer" 15 | TypeTransferToVesting OpType = "transfer_to_vesting" 16 | TypeWithdrawVesting OpType = "withdraw_vesting" 17 | TypeLimitOrderCreate OpType = "limit_order_create" 18 | TypeLimitOrderCancel OpType = "limit_order_cancel" 19 | TypeFeedPublish OpType = "feed_publish" 20 | TypeConvert OpType = "convert" 21 | TypeAccountCreate OpType = "account_create" 22 | TypeAccountUpdate OpType = "account_update" 23 | TypeWitnessUpdate OpType = "witness_update" 24 | TypeAccountWitnessVote OpType = "account_witness_vote" 25 | TypeAccountWitnessProxy OpType = "account_witness_proxy" 26 | TypePOW OpType = "pow" 27 | TypeCustom OpType = "custom" 28 | TypeReportOverProduction OpType = "report_over_production" 29 | TypeDeleteComment OpType = "delete_comment" 30 | TypeCustomJSON OpType = "custom_json" 31 | TypeCommentOptions OpType = "comment_options" 32 | TypeSetWithdrawVestingRoute OpType = "set_withdraw_vesting_route" 33 | TypeLimitOrderCreate2 OpType = "limit_order_create2" 34 | TypeChallengeAuthority OpType = "challenge_authority" 35 | TypeProveAuthority OpType = "prove_authority" 36 | TypeRequestAccountRecoverty OpType = "request_account_recovery" 37 | TypeRecoverAccount OpType = "recover_account" 38 | TypeChangeRecoveryAccount OpType = "change_recover_account" 39 | TypeEscrowTransfer OpType = "escrow_transfer" 40 | TypeEscrowDispute OpType = "escrow_dispute" 41 | TypeEscrowRelease OpType = "escrow_release" 42 | TypePOW2 OpType = "pow2" 43 | ) 44 | 45 | var opTypes = [...]OpType{ 46 | TypeVote, 47 | TypeComment, 48 | TypeTransfer, 49 | TypeTransferToVesting, 50 | TypeWithdrawVesting, 51 | TypeLimitOrderCreate, 52 | TypeLimitOrderCancel, 53 | TypeFeedPublish, 54 | TypeConvert, 55 | TypeAccountCreate, 56 | TypeAccountUpdate, 57 | TypeWitnessUpdate, 58 | TypeAccountWitnessVote, 59 | TypeAccountWitnessProxy, 60 | TypePOW, 61 | TypeCustom, 62 | TypeReportOverProduction, 63 | TypeDeleteComment, 64 | TypeCustomJSON, 65 | TypeCommentOptions, 66 | TypeSetWithdrawVestingRoute, 67 | TypeLimitOrderCreate2, 68 | TypeChallengeAuthority, 69 | TypeProveAuthority, 70 | TypeRequestAccountRecoverty, 71 | TypeRecoverAccount, 72 | TypeChangeRecoveryAccount, 73 | TypeEscrowTransfer, 74 | TypeEscrowDispute, 75 | TypeEscrowRelease, 76 | TypePOW2, 77 | } 78 | 79 | // opCodes keeps mapping operation type -> operation code. 80 | var opCodes map[OpType]uint16 81 | 82 | func init() { 83 | opCodes = make(map[OpType]uint16, len(opTypes)) 84 | for i, opType := range opTypes { 85 | opCodes[opType] = uint16(i) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/upvote/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | // Stdlib 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | // RPC 13 | "github.com/go-steem/rpc" 14 | "github.com/go-steem/rpc/encoding/wif" 15 | "github.com/go-steem/rpc/transactions" 16 | "github.com/go-steem/rpc/transports/websocket" 17 | "github.com/go-steem/rpc/types" 18 | 19 | // Vendor 20 | "github.com/pkg/errors" 21 | "golang.org/x/crypto/ssh/terminal" 22 | ) 23 | 24 | func main() { 25 | if err := run(); err != nil { 26 | log.Fatalln("Error:", err) 27 | } 28 | } 29 | 30 | func run() (err error) { 31 | // Process flags. 32 | flagAddress := flag.String("rpc_endpoint", "ws://localhost:8090", "steemd RPC endpoint address") 33 | flag.Parse() 34 | 35 | url := *flagAddress 36 | 37 | // Process args. 38 | args := flag.Args() 39 | if len(args) != 3 { 40 | return errors.New("3 arguments required") 41 | } 42 | author, permlink, voter := args[0], args[1], args[2] 43 | 44 | // Prompt for WIF. 45 | wifKey, err := promptWIF(voter) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | // Start catching signals. 51 | var interrupted bool 52 | signalCh := make(chan os.Signal, 1) 53 | signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) 54 | 55 | // Drop the error in case it is a request being interrupted. 56 | defer func() { 57 | if err == websocket.ErrClosing && interrupted { 58 | err = nil 59 | } 60 | }() 61 | 62 | // Instantiate the WebSocket transport. 63 | t, err := websocket.NewTransport(url) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // Use the transport to get an RPC client. 69 | client, err := rpc.NewClient(t) 70 | if err != nil { 71 | return err 72 | } 73 | defer func() { 74 | if !interrupted { 75 | client.Close() 76 | } 77 | }() 78 | 79 | // Start processing signals. 80 | go func() { 81 | <-signalCh 82 | fmt.Println() 83 | log.Println("Signal received, exiting...") 84 | signal.Stop(signalCh) 85 | interrupted = true 86 | client.Close() 87 | }() 88 | 89 | // Get the props to get the head block number and ID 90 | // so that we can use that for the transaction. 91 | props, err := client.Database.GetDynamicGlobalProperties() 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // Prepare the transaction. 97 | refBlockPrefix, err := transactions.RefBlockPrefix(props.HeadBlockID) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | tx := transactions.NewSignedTransaction(&types.Transaction{ 103 | RefBlockNum: transactions.RefBlockNum(props.HeadBlockNumber), 104 | RefBlockPrefix: refBlockPrefix, 105 | }) 106 | 107 | tx.PushOperation(&types.VoteOperation{ 108 | Voter: voter, 109 | Author: author, 110 | Permlink: permlink, 111 | Weight: 10000, 112 | }) 113 | 114 | // Sign. 115 | privKey, err := wif.Decode(wifKey) 116 | if err != nil { 117 | return err 118 | } 119 | privKeys := [][]byte{privKey} 120 | 121 | if err := tx.Sign(privKeys, transactions.SteemChain); err != nil { 122 | return err 123 | } 124 | 125 | // Broadcast. 126 | resp, err := client.NetworkBroadcast.BroadcastTransactionSynchronous(tx.Transaction) 127 | if err != nil { 128 | return err 129 | } 130 | fmt.Printf("%+v\n", *resp) 131 | 132 | // Success! 133 | return nil 134 | } 135 | 136 | func promptWIF(accountName string) (string, error) { 137 | fmt.Printf("Please insert WIF for account @%v: ", accountName) 138 | passwd, err := terminal.ReadPassword(syscall.Stdin) 139 | if err != nil { 140 | return "", errors.Wrap(err, "failed to read WIF from the terminal") 141 | } 142 | fmt.Println() 143 | return string(passwd), nil 144 | } 145 | -------------------------------------------------------------------------------- /transactions/signing.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "secp256k1.h" 6 | #include "secp256k1_recovery.h" 7 | 8 | #include "signing.h" 9 | 10 | static int sign( 11 | const secp256k1_context* ctx, 12 | const unsigned char *digest, 13 | const unsigned char *privkey, 14 | const void *ndata, 15 | unsigned char *signature, 16 | int *recid 17 | ); 18 | 19 | static bool is_canonical(const unsigned char *signature); 20 | 21 | void dump(const unsigned char *array, int len) { 22 | for (int i = 0; i < len; i++) { 23 | printf("%d ", array[i]); 24 | } 25 | printf("\n"); 26 | } 27 | 28 | int sign_transaction( 29 | const unsigned char *digest, 30 | const unsigned char *privkey, 31 | unsigned char *signature, 32 | int *recid 33 | ) { 34 | secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); 35 | 36 | int ndata = 1; 37 | 38 | while (1) { 39 | // Sign the transaction. 40 | if (!sign(ctx, digest, privkey, &ndata, signature, recid)) { 41 | secp256k1_context_destroy(ctx); 42 | return 0; 43 | } 44 | 45 | // Check whether the signiture is canonical. 46 | if (is_canonical(signature)) { 47 | *recid += 4; // compressed 48 | *recid += 27; // compact 49 | break; 50 | } 51 | 52 | ndata++; 53 | } 54 | 55 | secp256k1_context_destroy(ctx); 56 | return 1; 57 | } 58 | 59 | static int sign( 60 | const secp256k1_context* ctx, 61 | const unsigned char *digest, 62 | const unsigned char *privkey, 63 | const void *ndata, 64 | unsigned char *signature, 65 | int *recid 66 | ) { 67 | //printf("DIGEST:\n"); 68 | //dump(digest, 32); 69 | //printf("KEY:\n"); 70 | //dump(privkey, 32); 71 | 72 | // Prepare a signature. 73 | secp256k1_ecdsa_recoverable_signature sig; 74 | 75 | // Sign the digest using the given private key. 76 | if (!secp256k1_ecdsa_sign_recoverable(ctx, &sig, digest, privkey, NULL, ndata)) { 77 | return 0; 78 | } 79 | 80 | //printf("SIGNATURE DATA:\n"); 81 | //dump(sig.data, 65); 82 | 83 | // Serialize and return success. 84 | secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, signature, recid, &sig); 85 | return 1; 86 | } 87 | 88 | static bool is_canonical(const unsigned char *sig) { 89 | return (!(sig[0] & 0x80) && 90 | !(sig[0] == 0 && !(sig[1] & 0x80)) && 91 | !(sig[32] & 0x80) && 92 | !(sig[32] == 0 && !(sig[33] & 0x80))); 93 | } 94 | 95 | int verify_recoverable_signature( 96 | const unsigned char *digest, 97 | const unsigned char *signature, 98 | int recid, 99 | unsigned char *rawpubkey 100 | ) { 101 | // Get a context. 102 | secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY); 103 | 104 | // Parse the signature. 105 | secp256k1_ecdsa_recoverable_signature sig; 106 | 107 | if (!secp256k1_ecdsa_recoverable_signature_parse_compact(ctx, &sig, signature, recid)) { 108 | secp256k1_context_destroy(ctx); 109 | return 0; 110 | } 111 | 112 | // Recover the public key. 113 | secp256k1_pubkey pubkey; 114 | 115 | if (!secp256k1_ecdsa_recover(ctx, &pubkey, &sig, digest)) { 116 | secp256k1_context_destroy(ctx); 117 | return 0; 118 | } 119 | 120 | // Conver recoverable signature to normal signature. 121 | secp256k1_ecdsa_signature normsig; 122 | 123 | secp256k1_ecdsa_recoverable_signature_convert(ctx, &normsig, &sig); 124 | 125 | // Verify. 126 | if (!secp256k1_ecdsa_verify(ctx, &normsig, digest, &pubkey)) { 127 | secp256k1_context_destroy(ctx); 128 | return 0; 129 | } 130 | 131 | // Pass the public key back. 132 | size_t len = 33; 133 | secp256k1_ec_pubkey_serialize(ctx, rawpubkey, &len, &pubkey, SECP256K1_EC_COMPRESSED); 134 | 135 | // Clean up. 136 | secp256k1_context_destroy(ctx); 137 | return 1; 138 | } 139 | -------------------------------------------------------------------------------- /examples/voting_monitor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/go-steem/rpc" 13 | "github.com/go-steem/rpc/transports/websocket" 14 | "github.com/go-steem/rpc/types" 15 | ) 16 | 17 | func main() { 18 | if err := run(); err != nil { 19 | log.Fatalln("Error:", err) 20 | } 21 | } 22 | 23 | func run() (err error) { 24 | // Process flags. 25 | flagAddress := flag.String("rpc_endpoint", "ws://localhost:8090", "steemd RPC endpoint address") 26 | flagReconnect := flag.Bool("reconnect", false, "enable auto-reconnect mode") 27 | flag.Parse() 28 | 29 | var ( 30 | url = *flagAddress 31 | reconnect = *flagReconnect 32 | ) 33 | 34 | // Start catching signals. 35 | var interrupted bool 36 | signalCh := make(chan os.Signal, 1) 37 | signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM) 38 | 39 | // Drop the error in case it is a request being interrupted. 40 | defer func() { 41 | if err == websocket.ErrClosing && interrupted { 42 | err = nil 43 | } 44 | }() 45 | 46 | // Start the connection monitor. 47 | monitorChan := make(chan interface{}, 1) 48 | if reconnect { 49 | go func() { 50 | for { 51 | event, ok := <-monitorChan 52 | if ok { 53 | log.Println(event) 54 | } 55 | } 56 | }() 57 | } 58 | 59 | // Instantiate the WebSocket transport. 60 | log.Printf("---> Dial(\"%v\")\n", url) 61 | t, err := websocket.NewTransport(url, 62 | websocket.SetAutoReconnectEnabled(reconnect), 63 | websocket.SetAutoReconnectMaxDelay(30*time.Second), 64 | websocket.SetMonitor(monitorChan)) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Use the transport to get an RPC client. 70 | client, err := rpc.NewClient(t) 71 | if err != nil { 72 | return err 73 | } 74 | defer func() { 75 | if !interrupted { 76 | client.Close() 77 | } 78 | }() 79 | 80 | // Start processing signals. 81 | go func() { 82 | <-signalCh 83 | fmt.Println() 84 | log.Println("Signal received, exiting...") 85 | signal.Stop(signalCh) 86 | interrupted = true 87 | client.Close() 88 | }() 89 | 90 | // Get config. 91 | log.Println("---> GetConfig()") 92 | config, err := client.Database.GetConfig() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | // Use the last irreversible block number as the initial last block number. 98 | props, err := client.Database.GetDynamicGlobalProperties() 99 | if err != nil { 100 | return err 101 | } 102 | lastBlock := props.LastIrreversibleBlockNum 103 | 104 | // Keep processing incoming blocks forever. 105 | log.Printf("---> Entering the block processing loop (last block = %v)\n", lastBlock) 106 | for { 107 | // Get current properties. 108 | props, err := client.Database.GetDynamicGlobalProperties() 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // Process new blocks. 114 | for props.LastIrreversibleBlockNum-lastBlock > 0 { 115 | block, err := client.Database.GetBlock(lastBlock) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | // Process the transactions. 121 | for _, tx := range block.Transactions { 122 | for _, operation := range tx.Operations { 123 | switch op := operation.Data().(type) { 124 | case *types.VoteOperation: 125 | fmt.Printf("@%v voted for @%v/%v\n", op.Voter, op.Author, op.Permlink) 126 | 127 | // You can add more cases here, it depends on 128 | // what operations you actually need to process. 129 | } 130 | } 131 | } 132 | 133 | lastBlock++ 134 | } 135 | 136 | // Sleep for STEEMIT_BLOCK_INTERVAL seconds before the next iteration. 137 | time.Sleep(time.Duration(config.SteemitBlockInterval) * time.Second) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /transactions/signed_transaction.go: -------------------------------------------------------------------------------- 1 | // +build !nosigning 2 | 3 | package transactions 4 | 5 | import ( 6 | // Stdlib 7 | "bytes" 8 | "crypto/sha256" 9 | "encoding/hex" 10 | "time" 11 | "unsafe" 12 | 13 | // RPC 14 | "github.com/go-steem/rpc/encoding/transaction" 15 | "github.com/go-steem/rpc/types" 16 | 17 | // Vendor 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | // #cgo LDFLAGS: -lsecp256k1 22 | // #include 23 | // #include "signing.h" 24 | import "C" 25 | 26 | type SignedTransaction struct { 27 | *types.Transaction 28 | } 29 | 30 | func NewSignedTransaction(tx *types.Transaction) *SignedTransaction { 31 | if tx.Expiration == nil { 32 | expiration := time.Now().Add(30 * time.Second) 33 | tx.Expiration = &types.Time{&expiration} 34 | } 35 | 36 | return &SignedTransaction{tx} 37 | } 38 | 39 | func (tx *SignedTransaction) Serialize() ([]byte, error) { 40 | var b bytes.Buffer 41 | encoder := transaction.NewEncoder(&b) 42 | 43 | if err := encoder.Encode(tx.Transaction); err != nil { 44 | return nil, err 45 | } 46 | return b.Bytes(), nil 47 | } 48 | 49 | func (tx *SignedTransaction) Digest(chain *Chain) ([]byte, error) { 50 | var msgBuffer bytes.Buffer 51 | 52 | // Write the chain ID. 53 | rawChainID, err := hex.DecodeString(chain.ID) 54 | if err != nil { 55 | return nil, errors.Wrapf(err, "failed to decode chain ID: %v", chain.ID) 56 | } 57 | 58 | if _, err := msgBuffer.Write(rawChainID); err != nil { 59 | return nil, errors.Wrap(err, "failed to write chain ID") 60 | } 61 | 62 | // Write the serialized transaction. 63 | rawTx, err := tx.Serialize() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if _, err := msgBuffer.Write(rawTx); err != nil { 69 | return nil, errors.Wrap(err, "failed to write serialized transaction") 70 | } 71 | 72 | // Compute the digest. 73 | digest := sha256.Sum256(msgBuffer.Bytes()) 74 | return digest[:], nil 75 | } 76 | 77 | func (tx *SignedTransaction) Sign(privKeys [][]byte, chain *Chain) error { 78 | digest, err := tx.Digest(chain) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // Sign. 84 | cDigest := C.CBytes(digest) 85 | defer C.free(cDigest) 86 | 87 | cKeys := make([]unsafe.Pointer, 0, len(privKeys)) 88 | for _, key := range privKeys { 89 | cKeys = append(cKeys, C.CBytes(key)) 90 | } 91 | defer func() { 92 | for _, cKey := range cKeys { 93 | C.free(cKey) 94 | } 95 | }() 96 | 97 | sigs := make([][]byte, 0, len(privKeys)) 98 | for _, cKey := range cKeys { 99 | var ( 100 | signature [64]byte 101 | recid C.int 102 | ) 103 | 104 | code := C.sign_transaction( 105 | (*C.uchar)(cDigest), (*C.uchar)(cKey), (*C.uchar)(&signature[0]), &recid) 106 | if code == 0 { 107 | return errors.New("sign_transaction returned 0") 108 | } 109 | 110 | sig := make([]byte, 65) 111 | sig[0] = byte(recid) 112 | copy(sig[1:], signature[:]) 113 | 114 | sigs = append(sigs, sig) 115 | } 116 | 117 | // Set the signature array in the transaction. 118 | sigsHex := make([]string, 0, len(sigs)) 119 | for _, sig := range sigs { 120 | sigsHex = append(sigsHex, hex.EncodeToString(sig)) 121 | } 122 | 123 | tx.Transaction.Signatures = sigsHex 124 | return nil 125 | } 126 | 127 | func (tx *SignedTransaction) Verify(pubKeys [][]byte, chain *Chain) (bool, error) { 128 | // Compute the digest, again. 129 | digest, err := tx.Digest(chain) 130 | if err != nil { 131 | return false, err 132 | } 133 | 134 | cDigest := C.CBytes(digest) 135 | defer C.free(cDigest) 136 | 137 | // Make sure to free memory. 138 | cSigs := make([]unsafe.Pointer, 0, len(tx.Signatures)) 139 | defer func() { 140 | for _, cSig := range cSigs { 141 | C.free(cSig) 142 | } 143 | }() 144 | 145 | // Collect verified public keys. 146 | pubKeysFound := make([][]byte, len(pubKeys)) 147 | for i, signature := range tx.Signatures { 148 | sig, err := hex.DecodeString(signature) 149 | if err != nil { 150 | return false, errors.Wrap(err, "failed to decode signature hex") 151 | } 152 | 153 | recoverParameter := sig[0] - 27 - 4 154 | sig = sig[1:] 155 | 156 | cSig := C.CBytes(sig) 157 | cSigs = append(cSigs, cSig) 158 | 159 | var publicKey [33]byte 160 | 161 | code := C.verify_recoverable_signature( 162 | (*C.uchar)(cDigest), 163 | (*C.uchar)(cSig), 164 | (C.int)(recoverParameter), 165 | (*C.uchar)(&publicKey[0]), 166 | ) 167 | if code == 1 { 168 | pubKeysFound[i] = publicKey[:] 169 | } 170 | } 171 | 172 | for i := range pubKeys { 173 | if !bytes.Equal(pubKeysFound[i], pubKeys[i]) { 174 | return false, nil 175 | } 176 | } 177 | return true, nil 178 | } 179 | -------------------------------------------------------------------------------- /apis/follow/api.go: -------------------------------------------------------------------------------- 1 | package follow 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | 7 | // RPC 8 | "github.com/go-steem/rpc/interfaces" 9 | "github.com/go-steem/rpc/internal/rpc" 10 | 11 | // Vendor 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const APIID = "follow_api" 16 | 17 | type API struct { 18 | id int 19 | caller interfaces.Caller 20 | } 21 | 22 | func NewAPI(caller interfaces.Caller) (*API, error) { 23 | id, err := rpc.GetNumericAPIID(caller, APIID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return &API{id, caller}, nil 28 | } 29 | 30 | func (api *API) call(method string, params, resp interface{}) error { 31 | return api.caller.Call("call", []interface{}{"follow_api", method, params}, resp) 32 | } 33 | 34 | func (api *API) GetFollowersRaw( 35 | accountName string, 36 | start string, 37 | kind string, 38 | limit uint16, 39 | ) (*json.RawMessage, error) { 40 | 41 | var resp json.RawMessage 42 | params := []interface{}{accountName, start, kind, limit} 43 | if err := api.call("get_followers", params, &resp); err != nil { 44 | return nil, errors.Wrap(err, "go-steem/rpc: follow_api: failed to call get_followers") 45 | } 46 | return &resp, nil 47 | } 48 | 49 | func (api *API) GetFollowers( 50 | accountName string, 51 | start string, 52 | kind string, 53 | limit uint16, 54 | ) ([]*FollowObject, error) { 55 | 56 | raw, err := api.GetFollowersRaw(accountName, start, kind, limit) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | var resp []*FollowObject 62 | if err := json.Unmarshal([]byte(*raw), &resp); err != nil { 63 | return nil, errors.Wrap( 64 | err, "go-steem/rpc: follow_api: failed to unmarshal get_followers response") 65 | } 66 | return resp, nil 67 | 68 | } 69 | 70 | func (api *API) GetFollowingRaw( 71 | accountName string, 72 | start string, 73 | kind string, 74 | limit uint16, 75 | ) (*json.RawMessage, error) { 76 | 77 | var resp json.RawMessage 78 | params := []interface{}{accountName, start, kind, limit} 79 | if err := api.call("get_following", params, &resp); err != nil { 80 | return nil, errors.Wrap(err, "go-steem/rpc: follow_api: failed to call get_following") 81 | } 82 | return &resp, nil 83 | } 84 | 85 | func (api *API) GetFollowing( 86 | accountName string, 87 | start string, 88 | kind string, 89 | limit uint16, 90 | ) ([]*FollowObject, error) { 91 | 92 | raw, err := api.GetFollowingRaw(accountName, start, kind, limit) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | var resp []*FollowObject 98 | if err := json.Unmarshal([]byte(*raw), &resp); err != nil { 99 | return nil, errors.Wrap( 100 | err, "go-steem/rpc: follow_api: failed to unmarshal get_following response") 101 | } 102 | return resp, nil 103 | } 104 | 105 | func (api *API) GetFeedEntriesRaw( 106 | accountName string, 107 | entryID uint32, 108 | limit uint16, 109 | ) (*json.RawMessage, error) { 110 | 111 | if limit == 0 { 112 | limit = 500 113 | } 114 | 115 | var resp json.RawMessage 116 | params := []interface{}{accountName, entryID, limit} 117 | if err := api.call("get_feed_entries", params, &resp); err != nil { 118 | return nil, errors.Wrap(err, "go-steem/rpc: follow_api: failed to call get_feed_entries") 119 | } 120 | return &resp, nil 121 | } 122 | 123 | func (api *API) GetFeedEntries( 124 | accountName string, 125 | entryID uint32, 126 | limit uint16, 127 | ) ([]*FeedEntry, error) { 128 | 129 | raw, err := api.GetFeedEntriesRaw(accountName, entryID, limit) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | var resp []*FeedEntry 135 | if err := json.Unmarshal([]byte(*raw), &resp); err != nil { 136 | return nil, errors.Wrap( 137 | err, "go-steem/rpc: follow_api: failed to unmarshal get_feed_entries response") 138 | } 139 | return resp, nil 140 | } 141 | 142 | func (api *API) GetFeedRaw( 143 | accountName string, 144 | entryID uint32, 145 | limit uint16, 146 | ) (*json.RawMessage, error) { 147 | 148 | if limit == 0 { 149 | limit = 500 150 | } 151 | 152 | var resp json.RawMessage 153 | params := []interface{}{accountName, entryID, limit} 154 | if err := api.call("get_feed", params, &resp); err != nil { 155 | return nil, errors.Wrap(err, "go-steem/rpc: follow_api: failed to call get_feed") 156 | } 157 | return &resp, nil 158 | } 159 | 160 | func (api *API) GetAccountReputationsRaw( 161 | lowerBoundName string, 162 | limit uint32, 163 | ) (*json.RawMessage, error) { 164 | 165 | if limit == 0 { 166 | limit = 1000 167 | } 168 | 169 | var resp json.RawMessage 170 | params := []interface{}{lowerBoundName, limit} 171 | if err := api.call("get_account_reputations", params, &resp); err != nil { 172 | return nil, errors.Wrap( 173 | err, "go-steem/rpc: follow_api: failed to call get_account_reputations") 174 | } 175 | return &resp, nil 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-steem/rpc 2 | 3 | [![GoDoc](https://godoc.org/github.com/go-steem/rpc?status.svg)](https://godoc.org/github.com/go-steem/rpc) 4 | 5 | Golang RPC client library for [Steem](https://steem.io). 6 | 7 | ## Compatibility 8 | 9 | `steemd 0.13.0` 10 | 11 | ## Usage 12 | 13 | ```go 14 | import "github.com/go-steem/rpc" 15 | ``` 16 | 17 | This package is still very much in development, so `gopkg.in` is not yet available. 18 | 19 | ## Installation 20 | 21 | This package calls [bitcoin-core/secp256k1](https://github.com/bitcoin-core/secp256k1) 22 | using CGO to implement signed transactions, so you need to install `secp256k1` first. 23 | Then it will be possible to build `go-steem/rpc`. 24 | 25 | In case you don't need signed transactions, i.e. you don't need to use 26 | `network_broadcast_api`, it is possible to build the package with `nosigning` 27 | tag to exclude the functionality: 28 | 29 | ```bash 30 | $ go build -tags nosigning 31 | ``` 32 | 33 | ## Example 34 | 35 | This is just a code snippet. Please check the `examples` directory 36 | for more complete and ready to use examples. 37 | 38 | ```go 39 | // Instantiate the WebSocket transport. 40 | t, _ := websocket.NewTransport("ws://localhost:8090") 41 | 42 | // Use the transport to create an RPC client. 43 | client, _ := rpc.NewClient(t) 44 | defer client.Close() 45 | 46 | // Call "get_config". 47 | config, _ := client.Database.GetConfig() 48 | 49 | // Start processing blocks. 50 | lastBlock := 1800000 51 | for { 52 | // Call "get_dynamic_global_properties". 53 | props, _ := client.Database.GetDynamicGlobalProperties() 54 | 55 | for props.LastIrreversibleBlockNum-lastBlock > 0 { 56 | // Call "get_block". 57 | block, _ := client.Database.GetBlock(lastBlock) 58 | 59 | // Process the transactions. 60 | for _, tx := range block.Transactions { 61 | for _, op := range tx.Operations { 62 | switch body := op.Data().(type) { 63 | // Comment operation. 64 | case *types.CommentOperation: 65 | content, _ := client.Database.GetContent(body.Author, body.Permlink) 66 | fmt.Printf("COMMENT @%v %v\n", content.Author, content.URL) 67 | 68 | // Vote operation. 69 | case *types.VoteOperation: 70 | fmt.Printf("VOTE @%v @%v/%v\n", body.Voter, body.Author, body.Permlink) 71 | 72 | // You can add more cases, it depends on what 73 | // operations you actually need to process. 74 | } 75 | } 76 | } 77 | 78 | lastBlock++ 79 | } 80 | 81 | time.Sleep(time.Duration(config.SteemitBlockInterval) * time.Second) 82 | } 83 | ``` 84 | 85 | ## Package Organisation 86 | 87 | You need to create a `Client` object to be able to do anything. To be able to 88 | instantiate a `Client`, you first need to create a transport to be used to 89 | execute RPC calls. The WebSocket transport is available in `transports/websocket`. 90 | Then you just need to call `NewClient(transport)`. 91 | 92 | Once you create a `Client` object, you can start calling the methods exported 93 | via `steemd`'s RPC endpoint by invoking associated methods on the client object. 94 | There are multiple APIs that can be exported, e.g. `database_api` and `login_api`, 95 | so the methods on the Client object are also namespaced accoding to these APIs. 96 | For example, to call `get_block` from `database_api`, you need to use 97 | `Client.Database.GetBlock` method. 98 | 99 | When looking for a method to call, all you need is to turn the method name into 100 | CamelCase, e.g. `get_config` becomes `Client.Database.GetConfig`. 101 | 102 | ### Raw and Full Methods 103 | 104 | There are two methods implemented for every method exported via the RPC endpoint. 105 | The regular version and the raw version. Let's see an example for `get_config`: 106 | 107 | ```go 108 | func (client *Client) GetConfig() (*Config, error) { 109 | ... 110 | } 111 | 112 | func (client *Client) GetConfigRaw() (*json.RawMessage, error) { 113 | ... 114 | } 115 | ``` 116 | 117 | As we can see, the difference is that the raw version returns `*json.RawMessage`, 118 | so it is not trying to unmarshall the response into a properly typed response. 119 | 120 | There are two reasons for this: 121 | 122 | 1. To be able to see raw data. 123 | 2. To be able to call most of the remote methods even though the response 124 | object is not yet known or specified. 125 | 126 | It is already benefitial to just have the raw version because at least 127 | the method parameters are statically typed. 128 | 129 | ## Status 130 | 131 | This package is still under rapid development and it is by no means complete. 132 | For now there is no promise considering API stability. Some response objects 133 | maybe be typed incorrectly. The package is already usable, though. See the 134 | `examples` directory. 135 | 136 | To check the API coverate, please check the README files in the relevat API 137 | package in the `apis` subdirectory. 138 | 139 | ## License 140 | 141 | MIT, see the `LICENSE` file. 142 | -------------------------------------------------------------------------------- /types/operation.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | "reflect" 7 | 8 | // Vendor 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // dataObjects keeps mapping operation type -> operation data object. 13 | // This is used later on to unmarshal operation data based on the operation type. 14 | var dataObjects = map[OpType]Operation{ 15 | TypeVote: &VoteOperation{}, 16 | TypeComment: &CommentOperation{}, 17 | TypeTransfer: &TransferOperation{}, 18 | TypeTransferToVesting: &TransferToVestingOperation{}, 19 | TypeWithdrawVesting: &WithdrawVestingOperation{}, 20 | TypeLimitOrderCreate: &LimitOrderCreateOperation{}, 21 | TypeLimitOrderCancel: &LimitOrderCancelOperation{}, 22 | TypeFeedPublish: &FeedPublishOperation{}, 23 | TypeConvert: &ConvertOperation{}, 24 | TypeAccountCreate: &AccountCreateOperation{}, 25 | TypeAccountUpdate: &AccountUpdateOperation{}, 26 | // TypeWitnessUpdate: &WitnessUpdateOperation{}, 27 | TypeAccountWitnessVote: &AccountWitnessVoteOperation{}, 28 | TypeAccountWitnessProxy: &AccountWitnessProxyOperation{}, 29 | TypePOW: &POWOperation{}, 30 | // TypeCustom: &CustomOperation{}, 31 | TypeReportOverProduction: &ReportOverProductionOperation{}, 32 | TypeDeleteComment: &DeleteCommentOperation{}, 33 | TypeCustomJSON: &CustomJSONOperation{}, 34 | TypeCommentOptions: &CommentOptionsOperation{}, 35 | // TypeSetWithdrawVestingRoute: &SetWithdrawVestingRouteOperation{}, 36 | // TypeLimitOrderCreate2: &LimitOrderCreate2Operation{}, 37 | // TypeChallengeAuthority: &ChallengeAuthorityOperation{}, 38 | // TypeProveAuthority: &ProveAuthorityOperation{}, 39 | // TypeRequestAccountRecovery: &RequestAccountRecoveryOperation{}, 40 | // TypeRecoverAccount: &RecoverAccountOperation{}, 41 | // TypeChangeRecoveryAccount: &ChangeRecoverAccountOperation{}, 42 | // TypeEscrowTransfer: &EscrowTransferOperation{}, 43 | // TypeEscrowDispute: &EscrowDisputeOperation{}, 44 | // TypeEscrowRelease: &EescrowReleaseOperation{}, 45 | // TypePOW2: &POW2Operation{}, 46 | } 47 | 48 | // Operation represents an operation stored in a transaction. 49 | type Operation interface { 50 | // Type returns the operation type as present in the operation object, element [0]. 51 | Type() OpType 52 | 53 | // Data returns the operation data as present in the operation object, element [1]. 54 | // 55 | // When the operation type is known to this package, this field contains 56 | // the operation data object associated with the given operation type, 57 | // e.g. Type is TypeVote -> Data contains *VoteOperation. 58 | // Otherwise this field contains raw JSON (type *json.RawMessage). 59 | Data() interface{} 60 | } 61 | 62 | type Operations []Operation 63 | 64 | func (ops *Operations) UnmarshalJSON(data []byte) error { 65 | var tuples []*operationTuple 66 | if err := json.Unmarshal(data, &tuples); err != nil { 67 | return err 68 | } 69 | 70 | items := make([]Operation, 0, len(tuples)) 71 | for _, tuple := range tuples { 72 | items = append(items, tuple.Data) 73 | } 74 | 75 | *ops = items 76 | return nil 77 | } 78 | 79 | func (ops Operations) MarshalJSON() ([]byte, error) { 80 | tuples := make([]*operationTuple, 0, len(ops)) 81 | for _, op := range ops { 82 | tuples = append(tuples, &operationTuple{ 83 | Type: op.Type(), 84 | Data: op.Data().(Operation), 85 | }) 86 | } 87 | return json.Marshal(tuples) 88 | } 89 | 90 | type operationTuple struct { 91 | Type OpType 92 | Data Operation 93 | } 94 | 95 | func (op *operationTuple) MarshalJSON() ([]byte, error) { 96 | return json.Marshal([]interface{}{ 97 | op.Type, 98 | op.Data, 99 | }) 100 | } 101 | 102 | func (op *operationTuple) UnmarshalJSON(data []byte) error { 103 | // The operation object is [opType, opBody]. 104 | raw := make([]*json.RawMessage, 2) 105 | if err := json.Unmarshal(data, &raw); err != nil { 106 | return errors.Wrapf(err, "failed to unmarshal operation object: %v", string(data)) 107 | } 108 | if len(raw) != 2 { 109 | return errors.Errorf("invalid operation object: %v", string(data)) 110 | } 111 | 112 | // Unmarshal the type. 113 | var opType OpType 114 | if err := json.Unmarshal(*raw[0], &opType); err != nil { 115 | return errors.Wrapf(err, "failed to unmarshal Operation.Type: %v", string(*raw[0])) 116 | } 117 | 118 | // Unmarshal the data. 119 | var opData Operation 120 | template, ok := dataObjects[opType] 121 | if ok { 122 | opData = reflect.New( 123 | reflect.Indirect(reflect.ValueOf(template)).Type(), 124 | ).Interface().(Operation) 125 | 126 | if err := json.Unmarshal(*raw[1], opData); err != nil { 127 | return errors.Wrapf(err, "failed to unmarshal Operation.Data: %v", string(*raw[1])) 128 | } 129 | } else { 130 | opData = &UnknownOperation{opType, raw[1]} 131 | } 132 | 133 | // Update fields. 134 | op.Type = opType 135 | op.Data = opData 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /apis/database/README.md: -------------------------------------------------------------------------------- 1 | # Database API 2 | 3 | This package adds support for `database_api`. 4 | 5 | ## State 6 | 7 | The following subsections document the API completion. The method names 8 | are taken from `database_api.hpp` in `steemit/steem`. 9 | 10 | ### Subscriptions 11 | 12 | **TODO:** Is this actually callable over the RPC endpoint? 13 | It is a bit confusing to see `set_` prefix. Needs research. 14 | 15 | ``` 16 | (set_subscribe_callback) 17 | (set_pending_transaction_callback) 18 | (set_block_applied_callback) 19 | (cancel_all_subscriptions) 20 | ``` 21 | 22 | ### Tags 23 | 24 | | Method Name | Raw Version | Full Version | 25 | | --------------------------- |:-----------:|:------------:| 26 | | get_trending_tags | DONE | | 27 | | get_discussions_by_trending | DONE | | 28 | | get_discussions_by_created | DONE | | 29 | | get_discussions_by_active | DONE | | 30 | | get_discussions_by_cashout | DONE | | 31 | | get_discussions_by_payout | DONE | | 32 | | get_discussions_by_votes | DONE | | 33 | | get_discussions_by_children | DONE | | 34 | | get_discussions_by_hot | DONE | | 35 | | get_recommended_for | DONE | | 36 | 37 | ### Blocks and Transactions 38 | 39 | | Method Name | Raw Version | Full Version | 40 | | ----------------------- |:-----------:|:--------------:| 41 | | get_block_header | DONE | | 42 | | get_block | DONE | PARTIALLY DONE | 43 | | get_ops_in_block | DONE | DONE | 44 | | get_state | DONE | | 45 | | get_trending_categories | DONE | | 46 | | get_best_categories | DONE | | 47 | | get_active_categories | DONE | | 48 | | get_recent_categories | DONE | | 49 | | get_ops_in_block | DONE | | 50 | 51 | ### Globals 52 | 53 | | Method Name | Raw Version | Full Version | 54 | | -------------------------------- |:-----------:|:--------------:| 55 | | get_config | DONE | PARTIALLY DONE | 56 | | get_dynamic_global_properties | DONE | DONE | 57 | | get_chain_properties | DONE | | 58 | | get_feed_history | DONE | | 59 | | get_current_median_history_price | DONE | | 60 | | get_witness_schedule | DONE | | 61 | | get_hardfork_version | DONE | DONE | 62 | | get_next_scheduled_hardfork | DONE | | 63 | 64 | ### Keys 65 | 66 | | Method Name | Raw Version | Full Version | 67 | | ----------------- |:-----------:|:------------:| 68 | | get_key_reference | | | 69 | 70 | ### Accounts 71 | 72 | | Method Name | Raw Version | Full Version | 73 | | ------------------------- |:-----------:|:------------:| 74 | | get_accounts | DONE | | 75 | | get_account_references | | | 76 | | lookup_account_names | DONE | | 77 | | lookup_accounts | DONE | | 78 | | get_account_count | DONE | | 79 | | get_conversation_requests | DONE | | 80 | | get_account_history | DONE | | 81 | 82 | ### Market 83 | 84 | | Method Name | Raw Version | Full Version | 85 | | --------------- |:-----------:|:------------:| 86 | | get_order_book | | | 87 | | get_open_orders | | | 88 | 89 | ### Authority / Validation 90 | 91 | | Method Name | Raw Version | Full Version | 92 | | ------------------------ |:-----------:|:------------:| 93 | | get_transaction_hex | | | 94 | | get_transaction | | | 95 | | get_required_signatures | | | 96 | | get_potential_signatures | | | 97 | | verify_authority | | | 98 | | verity_account_authority | | | 99 | 100 | ### Votes 101 | 102 | | Method Name | Raw Version | Full Version | 103 | | ----------------- |:-----------:|:------------:| 104 | | get_active_votes | DONE | DONE | 105 | | get_account_votes | DONE | | 106 | 107 | ### Cotent 108 | 109 | | Method Name | Raw Version | Full Version | 110 | | ------------------------------------- |:-----------:|:--------------:| 111 | | get_content | DONE | PARTIALLY DONE | 112 | | get_content_replies | DONE | PARTIALLY DONE | 113 | | get_discussions_by_author_before_date | | | 114 | | get_replies_by_last_update | DONE | | 115 | 116 | ### Witnesses 117 | 118 | | Method Name | Raw Version | Full Version | 119 | | ----------------------- |:-----------:|:------------:| 120 | | get_witnesses | | | 121 | | get_witness_by_account | | | 122 | | get_witnesses_by_vote | | | 123 | | lookup_witness_accounts | | | 124 | | get_witness_count | | | 125 | | get_active_witnesses | | | 126 | | get_miner_queue | | | 127 | 128 | ## License 129 | 130 | MIT, see the `LICENSE` file. 131 | -------------------------------------------------------------------------------- /apis/database/data.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | "strconv" 7 | "strings" 8 | 9 | // RPC 10 | "github.com/go-steem/rpc/types" 11 | ) 12 | 13 | type Config struct { 14 | SteemitBlockchainHardforkVersion string `json:"STEEMIT_BLOCKCHAIN_HARDFORK_VERSION"` 15 | SteemitBlockchainVersion string `json:"STEEMIT_BLOCKCHAIN_VERSION"` 16 | SteemitBlockInterval uint `json:"STEEMIT_BLOCK_INTERVAL"` 17 | } 18 | 19 | type DynamicGlobalProperties struct { 20 | Time *types.Time `json:"time"` 21 | TotalPow *types.Int `json:"total_pow"` 22 | NumPowWitnesses *types.Int `json:"num_pow_witnesses"` 23 | CurrentReserveRatio *types.Int `json:"current_reserve_ratio"` 24 | ID *types.ID `json:"id"` 25 | CurrentSupply string `json:"current_supply"` 26 | CurrentSBDSupply string `json:"current_sbd_supply"` 27 | MaximumBlockSize *types.Int `json:"maximum_block_size"` 28 | RecentSlotsFilled *types.Int `json:"recent_slots_filled"` 29 | CurrentWitness string `json:"current_witness"` 30 | TotalRewardShares2 *types.Int `json:"total_reward_shares2"` 31 | AverageBlockSize *types.Int `json:"average_block_size"` 32 | CurrentAslot *types.Int `json:"current_aslot"` 33 | LastIrreversibleBlockNum uint32 `json:"last_irreversible_block_num"` 34 | TotalVestingShares string `json:"total_vesting_shares"` 35 | TotalVersingFundSteem string `json:"total_vesting_fund_steem"` 36 | HeadBlockID string `json:"head_block_id"` 37 | HeadBlockNumber types.UInt32 `json:"head_block_number"` 38 | VirtualSupply string `json:"virtual_supply"` 39 | ConfidentialSupply string `json:"confidential_supply"` 40 | ConfidentialSBDSupply string `json:"confidential_sbd_supply"` 41 | TotalRewardFundSteem string `json:"total_reward_fund_steem"` 42 | TotalActivityFundSteem string `json:"total_activity_fund_steem"` 43 | TotalActivityFundShares *types.Int `json:"total_activity_fund_shares"` 44 | SBDInterestRate *types.Int `json:"sbd_interest_rate"` 45 | MaxVirtualBandwidth *types.Int `json:"max_virtual_bandwidth"` 46 | } 47 | 48 | type Block struct { 49 | Number uint32 `json:"-"` 50 | Timestamp *types.Time `json:"timestamp"` 51 | Witness string `json:"witness"` 52 | WitnessSignature string `json:"witness_signature"` 53 | TransactionMerkleRoot string `json:"transaction_merkle_root"` 54 | Previous string `json:"previous"` 55 | Extensions [][]interface{} `json:"extensions"` 56 | Transactions []*types.Transaction `json:"transactions"` 57 | } 58 | 59 | type Content struct { 60 | Id *types.ID `json:"id"` 61 | RootTitle string `json:"root_title"` 62 | Active *types.Time `json:"active"` 63 | AbsRshares *types.Int `json:"abs_rshares"` 64 | PendingPayoutValue string `json:"pending_payout_value"` 65 | TotalPendingPayoutValue string `json:"total_pending_payout_value"` 66 | Category string `json:"category"` 67 | Title string `json:"title"` 68 | LastUpdate *types.Time `json:"last_update"` 69 | Stats string `json:"stats"` 70 | Body string `json:"body"` 71 | Created *types.Time `json:"created"` 72 | Replies []*Content `json:"replies"` 73 | Permlink string `json:"permlink"` 74 | JsonMetadata *ContentMetadata `json:"json_metadata"` 75 | Children *types.Int `json:"children"` 76 | NetRshares *types.Int `json:"net_rshares"` 77 | URL string `json:"url"` 78 | ActiveVotes []*VoteState `json:"active_votes"` 79 | ParentPermlink string `json:"parent_permlink"` 80 | CashoutTime *types.Time `json:"cashout_time"` 81 | TotalPayoutValue string `json:"total_payout_value"` 82 | ParentAuthor string `json:"parent_author"` 83 | ChildrenRshares2 *types.Int `json:"children_rshares2"` 84 | Author string `json:"author"` 85 | Depth *types.Int `json:"depth"` 86 | TotalVoteWeight *types.Int `json:"total_vote_weight"` 87 | } 88 | 89 | func (content *Content) IsStory() bool { 90 | return content.ParentAuthor == "" 91 | } 92 | 93 | type ContentMetadata struct { 94 | Flag bool 95 | Value string 96 | Users []string 97 | Tags []string 98 | Image []string 99 | } 100 | 101 | type ContentMetadataRaw struct { 102 | Users types.StringSlice `json:"users"` 103 | Tags types.StringSlice `json:"tags"` 104 | Image types.StringSlice `json:"image"` 105 | } 106 | 107 | func (metadata *ContentMetadata) UnmarshalJSON(data []byte) error { 108 | unquoted, err := strconv.Unquote(string(data)) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | switch unquoted { 114 | case "true": 115 | metadata.Flag = true 116 | return nil 117 | case "false": 118 | metadata.Flag = false 119 | return nil 120 | } 121 | 122 | if len(unquoted) == 0 { 123 | return nil 124 | } 125 | 126 | if unquoted[0] == '"' { 127 | metadata.Value = unquoted 128 | return nil 129 | } 130 | 131 | var raw ContentMetadataRaw 132 | if err := json.NewDecoder(strings.NewReader(unquoted)).Decode(&raw); err != nil { 133 | return err 134 | } 135 | 136 | metadata.Users = raw.Users 137 | metadata.Tags = raw.Tags 138 | metadata.Image = raw.Image 139 | 140 | return nil 141 | } 142 | 143 | type VoteState struct { 144 | Voter string `json:"voter"` 145 | Weight *types.Int `json:"weight"` 146 | Rshares *types.Int `json:"rshares"` 147 | Percent *types.Int `json:"percent"` 148 | Time *types.Time `json:"time"` 149 | } 150 | -------------------------------------------------------------------------------- /transports/websocket/transport.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "io" 5 | // Stdlib 6 | "context" 7 | "net" 8 | "time" 9 | 10 | // Vendor 11 | "github.com/gorilla/websocket" 12 | "github.com/pkg/errors" 13 | "github.com/sourcegraph/jsonrpc2" 14 | tomb "gopkg.in/tomb.v2" 15 | ) 16 | 17 | const ( 18 | DefaultHandshakeTimeout = 30 * time.Second 19 | DefaultWriteTimeout = 10 * time.Second 20 | DefaultReadTimeout = 20 * time.Second 21 | DefaultAutoReconnectMaxDelay = 1 * time.Minute 22 | 23 | InitialAutoReconnectDelay = 1 * time.Second 24 | AutoReconnectBackoffCoefficient = 1.5 25 | ) 26 | 27 | var netDialer net.Dialer 28 | 29 | // Transport implements a CallCloser accessing the Steem RPC endpoint over WebSocket. 30 | type Transport struct { 31 | // URLs as passed into the constructor. 32 | urls []string 33 | nextURLIndex int 34 | currentURL string 35 | 36 | // Options. 37 | handshakeTimeout time.Duration 38 | readTimeout time.Duration 39 | writeTimeout time.Duration 40 | 41 | autoReconnectEnabled bool 42 | autoReconnectMaxDelay time.Duration 43 | 44 | monitorChan chan<- interface{} 45 | 46 | // The underlying JSON-RPC connection. 47 | connCh chan chan *jsonrpc2.Conn 48 | errCh chan error 49 | 50 | t *tomb.Tomb 51 | } 52 | 53 | // Option represents an option that can be passed into the transport constructor. 54 | type Option func(*Transport) 55 | 56 | // SetDialTimeout can be used to set the timeout when establishing a new connection. 57 | // 58 | // This function is deprecated, please use SetHandshakeTimeout. 59 | func SetDialTimeout(timeout time.Duration) Option { 60 | return SetHandshakeTimeout(timeout) 61 | } 62 | 63 | // SetHandshakeTimeout can be used to set the timeout for WebSocket handshake. 64 | func SetHandshakeTimeout(timeout time.Duration) Option { 65 | return func(t *Transport) { 66 | t.handshakeTimeout = timeout 67 | } 68 | } 69 | 70 | // SetReadTimeout sets the connection read timeout. 71 | // The timeout is implemented using net.Conn.SetReadDeadline. 72 | func SetReadTimeout(timeout time.Duration) Option { 73 | return func(t *Transport) { 74 | t.readTimeout = timeout 75 | } 76 | } 77 | 78 | // SetWriteTimeout sets the connection read timeout. 79 | // The timeout is implemented using net.Conn.SetWriteDeadline. 80 | func SetWriteTimeout(timeout time.Duration) Option { 81 | return func(t *Transport) { 82 | t.writeTimeout = timeout 83 | } 84 | } 85 | 86 | // SetReadWriteTimeout sets the connection read and write timeout. 87 | // The timeout is implemented using net.Conn.SetDeadline. 88 | func SetReadWriteTimeout(timeout time.Duration) Option { 89 | return func(t *Transport) { 90 | t.readTimeout = timeout 91 | t.writeTimeout = timeout 92 | } 93 | } 94 | 95 | // SetAutoReconnectEnabled can be used to enable automatic reconnection to the RPC endpoint. 96 | // Exponential backoff is used when the connection cannot be established repetitively. 97 | // 98 | // See SetAutoReconnectMaxDelay to set the maximum delay between the reconnection attempts. 99 | func SetAutoReconnectEnabled(enabled bool) Option { 100 | return func(t *Transport) { 101 | t.autoReconnectEnabled = enabled 102 | } 103 | } 104 | 105 | // SetAutoReconnectMaxDelay can be used to set the maximum delay between the reconnection attempts. 106 | // 107 | // This option only takes effect when the auto-reconnect mode is enabled. 108 | // 109 | // The default value is 5 minutes. 110 | func SetAutoReconnectMaxDelay(delay time.Duration) Option { 111 | return func(t *Transport) { 112 | t.autoReconnectMaxDelay = delay 113 | } 114 | } 115 | 116 | // SetMonitor can be used to set the monitoring channel that can be used to watch 117 | // connection-related state changes. 118 | // 119 | // All channel send operations are happening synchronously, so not receiving messages 120 | // from the channel will lead to the whole thing getting stuck completely. 121 | // 122 | // This option only takes effect when the auto-reconnect mode is enabled. 123 | // 124 | // The channel is closed when the transport is closed. 125 | func SetMonitor(monitorChan chan<- interface{}) Option { 126 | return func(t *Transport) { 127 | t.monitorChan = monitorChan 128 | } 129 | } 130 | 131 | // NewTransport creates a new transport that connects to the given WebSocket URLs. 132 | // 133 | // It is possible to specify multiple WebSocket endpoint URLs. 134 | // In case the transport is configured to reconnect automatically, 135 | // the URL to connect to is rotated on every connect attempt using round-robin. 136 | func NewTransport(urls []string, options ...Option) (*Transport, error) { 137 | // Prepare a transport instance. 138 | t := &Transport{ 139 | urls: urls, 140 | handshakeTimeout: DefaultHandshakeTimeout, 141 | readTimeout: DefaultReadTimeout, 142 | writeTimeout: DefaultWriteTimeout, 143 | autoReconnectMaxDelay: DefaultAutoReconnectMaxDelay, 144 | connCh: make(chan chan *jsonrpc2.Conn), 145 | errCh: make(chan error), 146 | t: &tomb.Tomb{}, 147 | } 148 | 149 | // Apply the options. 150 | for _, opt := range options { 151 | opt(t) 152 | } 153 | 154 | t.t.Go(t.dialer) 155 | 156 | // Return the new transport. 157 | return t, nil 158 | } 159 | 160 | // Call implements interfaces.CallCloser. 161 | func (t *Transport) Call(method string, params, result interface{}) error { 162 | // Limit the request context with the tomb context. 163 | ctx := t.t.Context(nil) 164 | 165 | Loop: 166 | for { 167 | // Request a connection. 168 | connCh := make(chan *jsonrpc2.Conn, 1) 169 | select { 170 | case t.connCh <- connCh: 171 | case <-ctx.Done(): 172 | return errors.Wrap(ctx.Err(), "context closed") 173 | } 174 | 175 | // Receive the connection. 176 | conn := <-connCh 177 | 178 | // Perform the call. 179 | err := conn.Call(ctx, method, params, result) 180 | if err == nil { 181 | return nil 182 | } 183 | 184 | // In case this is a context error, return immediately. 185 | if err := ctx.Err(); err != nil { 186 | return errors.Wrap(err, "context closed") 187 | } 188 | 189 | // In case auto-reconnect is disabled, fail immediately. 190 | if !t.autoReconnectEnabled { 191 | return errors.Wrap(err, "call failed") 192 | } 193 | 194 | // In case this is a connection error, request a new connection. 195 | err = errors.Cause(err) 196 | if _, ok := err.(*websocket.CloseError); ok || err == io.ErrUnexpectedEOF { 197 | select { 198 | case t.errCh <- errors.Wrap(err, "WebSocket closed"): 199 | continue Loop 200 | case <-ctx.Done(): 201 | return errors.Wrap(ctx.Err(), "context closed") 202 | } 203 | } 204 | 205 | // Some other error occurred, return it immediately. 206 | return errors.Wrap(err, "call failed") 207 | } 208 | } 209 | 210 | func (t *Transport) dialer() error { 211 | ctx := t.t.Context(nil) 212 | 213 | var conn *jsonrpc2.Conn 214 | defer func() { 215 | if conn != nil { 216 | conn.Close() 217 | err := errors.Wrap(ctx.Err(), "context closed") 218 | t.emit(&DisconnectedEvent{t.currentURL, err}) 219 | } 220 | }() 221 | 222 | connect := func() { 223 | delay := InitialAutoReconnectDelay 224 | 225 | for { 226 | var err error 227 | conn, err = t.dial(ctx) 228 | if err == nil { 229 | break 230 | } 231 | 232 | select { 233 | case <-time.After(delay): 234 | delay = time.Duration(float64(delay) * AutoReconnectBackoffCoefficient) 235 | if delay > t.autoReconnectMaxDelay { 236 | delay = t.autoReconnectMaxDelay 237 | } 238 | 239 | case <-ctx.Done(): 240 | return 241 | } 242 | } 243 | } 244 | 245 | // Establish the initial connection. 246 | connect() 247 | 248 | for { 249 | select { 250 | case connCh := <-t.connCh: 251 | connCh <- conn 252 | 253 | case err := <-t.errCh: 254 | conn.Close() 255 | t.emit(&DisconnectedEvent{t.currentURL, err}) 256 | connect() 257 | 258 | case <-ctx.Done(): 259 | return nil 260 | } 261 | } 262 | } 263 | 264 | func (t *Transport) dial(ctx context.Context) (*jsonrpc2.Conn, error) { 265 | // Set up a dialer. 266 | dialer := websocket.Dialer{ 267 | NetDial: func(network, addr string) (net.Conn, error) { 268 | return netDialer.DialContext(ctx, network, addr) 269 | }, 270 | HandshakeTimeout: t.handshakeTimeout, 271 | } 272 | 273 | // Get the next URL to try. 274 | u := t.urls[t.nextURLIndex] 275 | t.nextURLIndex = (t.nextURLIndex + 1) % len(t.urls) 276 | t.currentURL = u 277 | 278 | // Connect the WebSocket. 279 | t.emit(&ConnectingEvent{u}) 280 | ws, _, err := dialer.Dial(u, nil) 281 | if err != nil { 282 | err = errors.Wrapf(err, "failed to dial %v", u) 283 | t.emit(&DisconnectedEvent{u, err}) 284 | return nil, err 285 | } 286 | t.emit(&ConnectedEvent{u}) 287 | 288 | // Wrap the WebSocket with JSON-RPC2. 289 | stream := NewObjectStream(ws, t.writeTimeout, t.readTimeout) 290 | return jsonrpc2.NewConn(ctx, stream, nil), nil 291 | } 292 | 293 | func (t *Transport) emit(v interface{}) { 294 | if t.monitorChan != nil { 295 | select { 296 | case t.monitorChan <- v: 297 | default: 298 | } 299 | } 300 | } 301 | 302 | // Close implements interfaces.CallCloser. 303 | func (t *Transport) Close() error { 304 | t.t.Kill(nil) 305 | return t.t.Wait() 306 | } 307 | -------------------------------------------------------------------------------- /apis/database/api.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | "errors" 7 | 8 | // RPC 9 | "github.com/go-steem/rpc/interfaces" 10 | "github.com/go-steem/rpc/internal/call" 11 | "github.com/go-steem/rpc/types" 12 | ) 13 | 14 | const ( 15 | APIID = "database_api" 16 | NumbericAPIID = 0 17 | ) 18 | 19 | type API struct { 20 | caller interfaces.Caller 21 | } 22 | 23 | func NewAPI(caller interfaces.Caller) *API { 24 | return &API{caller} 25 | } 26 | 27 | /* 28 | // Subscriptions 29 | (set_subscribe_callback) 30 | (set_pending_transaction_callback) 31 | (set_block_applied_callback) 32 | (cancel_all_subscriptions) 33 | */ 34 | 35 | /* 36 | // Tags 37 | (get_trending_tags) 38 | (get_discussions_by_trending) 39 | (get_discussions_by_created) 40 | (get_discussions_by_active) 41 | (get_discussions_by_cashout) 42 | (get_discussions_by_payout) 43 | (get_discussions_by_votes) 44 | (get_discussions_by_children) 45 | (get_discussions_by_hot) 46 | (get_recommended_for) 47 | */ 48 | 49 | func (api *API) GetTrendingTagsRaw(afterTag string, limit uint32) (*json.RawMessage, error) { 50 | return call.Raw(api.caller, "get_trending_tags", []interface{}{afterTag, limit}) 51 | } 52 | 53 | type DiscussionQuery struct { 54 | Tag string `json:"tag"` 55 | Limit uint32 `json:"limit"` 56 | // XXX: Not sure about the type here. 57 | FilterTags []string `json:"filter_tags"` 58 | StartAuthor string `json:"start_author,omitempty"` 59 | StartPermlink string `json:"start_permlink,omitempty"` 60 | ParentAuthor string `json:"parent_author,omitempty"` 61 | ParentPermlink string `json:"parent_permlink"` 62 | } 63 | 64 | func (api *API) GetDiscussionsByTrendingRaw(query *DiscussionQuery) (*json.RawMessage, error) { 65 | return call.Raw(api.caller, "get_discussions_by_trending", query) 66 | } 67 | 68 | func (api *API) GetDiscussionsByCreatedRaw(query *DiscussionQuery) (*json.RawMessage, error) { 69 | return call.Raw(api.caller, "get_discussions_by_created", query) 70 | } 71 | 72 | func (api *API) GetDiscussionsByActiveRaw(query *DiscussionQuery) (*json.RawMessage, error) { 73 | return call.Raw(api.caller, "get_discussions_by_active", query) 74 | } 75 | 76 | func (api *API) GetDiscussionsByCashoutRaw(query *DiscussionQuery) (*json.RawMessage, error) { 77 | return call.Raw(api.caller, "get_discussions_by_cashout", query) 78 | } 79 | 80 | func (api *API) GetDiscussionsByPayoutRaw(query *DiscussionQuery) (*json.RawMessage, error) { 81 | return call.Raw(api.caller, "get_discussions_by_payout", query) 82 | } 83 | 84 | func (api *API) GetDiscussionsByVotesRaw(query *DiscussionQuery) (*json.RawMessage, error) { 85 | return call.Raw(api.caller, "get_discussions_by_votes", query) 86 | } 87 | 88 | func (api *API) GetDiscussionsByChildrenRaw(query *DiscussionQuery) (*json.RawMessage, error) { 89 | return call.Raw(api.caller, "get_discussions_by_children", query) 90 | } 91 | 92 | func (api *API) GetDiscussionsByHotRaw(query *DiscussionQuery) (*json.RawMessage, error) { 93 | return call.Raw(api.caller, "get_discussions_by_hot", query) 94 | } 95 | 96 | func (api *API) GetRecommendedForRaw(user string, limit uint32) (*json.RawMessage, error) { 97 | return call.Raw(api.caller, "get_discussions_by_votes", []interface{}{user, limit}) 98 | } 99 | 100 | /* 101 | // Blocks and transactions 102 | (get_block_header) 103 | (get_block) 104 | (get_state) 105 | (get_trending_categories) 106 | (get_best_categories) 107 | (get_active_categories) 108 | (get_recent_categories) 109 | */ 110 | 111 | func (api *API) GetBlockHeaderRaw(blockNum uint32) (*json.RawMessage, error) { 112 | return call.Raw(api.caller, "get_block_header", []uint32{blockNum}) 113 | } 114 | 115 | func (api *API) GetBlockRaw(blockNum uint32) (*json.RawMessage, error) { 116 | return call.Raw(api.caller, "get_block", []uint32{blockNum}) 117 | } 118 | 119 | func (api *API) GetBlock(blockNum uint32) (*Block, error) { 120 | var resp Block 121 | if err := api.caller.Call("get_block", []uint32{blockNum}, &resp); err != nil { 122 | return nil, err 123 | } 124 | resp.Number = blockNum 125 | return &resp, nil 126 | } 127 | 128 | func (api *API) GetStateRaw(path string) (*json.RawMessage, error) { 129 | return call.Raw(api.caller, "get_state", []string{path}) 130 | } 131 | 132 | func (api *API) GetTrendingCategoriesRaw(after string, limit uint32) (*json.RawMessage, error) { 133 | return call.Raw(api.caller, "get_trending_categories", []interface{}{after, limit}) 134 | } 135 | 136 | func (api *API) GetBestCategoriesRaw(after string, limit uint32) (*json.RawMessage, error) { 137 | return call.Raw(api.caller, "get_best_categories", []interface{}{after, limit}) 138 | } 139 | 140 | func (api *API) GetActiveCategoriesRaw(after string, limit uint32) (*json.RawMessage, error) { 141 | return call.Raw(api.caller, "get_active_categories", []interface{}{after, limit}) 142 | } 143 | 144 | func (api *API) GetRecentCategoriesRaw(after string, limit uint32) (*json.RawMessage, error) { 145 | return call.Raw(api.caller, "get_recent_categories", []interface{}{after, limit}) 146 | } 147 | 148 | /* 149 | // Globals 150 | (get_config) 151 | (get_dynamic_global_properties) 152 | (get_chain_properties) 153 | (get_feed_history) 154 | (get_current_median_history_price) 155 | (get_witness_schedule) 156 | (get_hardfork_version) 157 | (get_next_scheduled_hardfork) 158 | */ 159 | 160 | func (api *API) GetConfigRaw() (*json.RawMessage, error) { 161 | return call.Raw(api.caller, "get_config", call.EmptyParams) 162 | } 163 | 164 | func (api *API) GetConfig() (*Config, error) { 165 | var resp Config 166 | if err := api.caller.Call("get_config", call.EmptyParams, &resp); err != nil { 167 | return nil, err 168 | } 169 | return &resp, nil 170 | } 171 | 172 | func (api *API) GetDynamicGlobalPropertiesRaw() (*json.RawMessage, error) { 173 | return call.Raw(api.caller, "get_dynamic_global_properties", call.EmptyParams) 174 | } 175 | 176 | func (api *API) GetDynamicGlobalProperties() (*DynamicGlobalProperties, error) { 177 | var resp DynamicGlobalProperties 178 | if err := api.caller.Call("get_dynamic_global_properties", call.EmptyParams, &resp); err != nil { 179 | return nil, err 180 | } 181 | return &resp, nil 182 | } 183 | 184 | func (api *API) GetChainPropertiesRaw() (*json.RawMessage, error) { 185 | return call.Raw(api.caller, "get_chain_properties", call.EmptyParams) 186 | } 187 | 188 | func (api *API) GetFeedHistoryRaw() (*json.RawMessage, error) { 189 | return call.Raw(api.caller, "get_feed_history", call.EmptyParams) 190 | } 191 | 192 | func (api *API) GetCurrentMedianHistoryPriceRaw() (*json.RawMessage, error) { 193 | return call.Raw(api.caller, "get_current_median_history_price", call.EmptyParams) 194 | } 195 | 196 | func (api *API) GetWitnessScheduleRaw() (*json.RawMessage, error) { 197 | return call.Raw(api.caller, "get_witness_schedule", call.EmptyParams) 198 | } 199 | 200 | func (api *API) GetHardforkVersionRaw() (*json.RawMessage, error) { 201 | return call.Raw(api.caller, "get_hardfork_version", call.EmptyParams) 202 | } 203 | 204 | func (api *API) GetHardforkVersion() (string, error) { 205 | var resp string 206 | if err := api.caller.Call("get_hardfork_version", call.EmptyParams, &resp); err != nil { 207 | return "", err 208 | } 209 | return resp, nil 210 | } 211 | 212 | func (api *API) GetNextScheduledHardforkRaw() (*json.RawMessage, error) { 213 | return call.Raw(api.caller, "get_next_scheduled_hardfork", call.EmptyParams) 214 | } 215 | 216 | /* 217 | // Keys 218 | (get_key_references) 219 | */ 220 | 221 | // XXX: Not sure about params. 222 | //func (api *API) GetKeyReferencesRaw(key []string) (*json.RawMessage, error) { 223 | // return call.Raw(api.caller, "get_key_references", [][]string{key}) 224 | //} 225 | 226 | /* 227 | // Accounts 228 | (get_accounts) 229 | (get_account_references) 230 | (lookup_account_names) 231 | (lookup_accounts) 232 | (get_account_count) 233 | (get_conversion_requests) 234 | (get_account_history) 235 | */ 236 | 237 | func (api *API) GetAccountsRaw(accountNames []string) (*json.RawMessage, error) { 238 | return call.Raw(api.caller, "get_accounts", [][]string{accountNames}) 239 | } 240 | 241 | // XXX: Not sure about params. 242 | //func (api *API) GetAccountReferenceRaw(id string) (*json.RawMessage, error) { 243 | // return call.Raw(api.caller, "get_account_reference", []string{id}) 244 | //} 245 | 246 | func (api *API) LookupAccountNamesRaw(accountNames []string) (*json.RawMessage, error) { 247 | return call.Raw(api.caller, "lookup_account_names", [][]string{accountNames}) 248 | } 249 | 250 | func (api *API) LookupAccountsRaw(lowerBoundName string, limit uint32) (*json.RawMessage, error) { 251 | return call.Raw(api.caller, "lookup_accounts", []interface{}{lowerBoundName, limit}) 252 | } 253 | 254 | func (api *API) GetAccountCountRaw() (*json.RawMessage, error) { 255 | return call.Raw(api.caller, "get_account_count", call.EmptyParams) 256 | } 257 | 258 | func (api *API) GetConversionRequestsRaw(accountName string) (*json.RawMessage, error) { 259 | return call.Raw(api.caller, "get_conversion_requests", []string{accountName}) 260 | } 261 | 262 | func (api *API) GetAccountHistoryRaw(account string, from uint64, limit uint32) (*json.RawMessage, error) { 263 | return call.Raw(api.caller, "get_account_history", []interface{}{account, from, limit}) 264 | } 265 | 266 | /* 267 | // Market 268 | (get_order_book) 269 | */ 270 | 271 | func (api *API) GetOrderBookRaw(limit uint32) (*json.RawMessage, error) { 272 | if limit > 1000 { 273 | return nil, errors.New("GetOrderBook: limit must not exceed 1000") 274 | } 275 | return call.Raw(api.caller, "get_order_book", []interface{}{limit}) 276 | } 277 | 278 | /* 279 | // Authority / validation 280 | (get_transaction_hex) 281 | (get_transaction) 282 | (get_required_signatures) 283 | (get_potential_signatures) 284 | (verify_authority) 285 | (verify_account_authority) 286 | */ 287 | 288 | /* 289 | // Votes 290 | (get_active_votes) 291 | (get_account_votes) 292 | */ 293 | 294 | func (api *API) GetActiveVotesRaw(author, permlink string) (*json.RawMessage, error) { 295 | return call.Raw(api.caller, "get_active_votes", []string{author, permlink}) 296 | } 297 | 298 | func (api *API) GetActiveVotes(author, permlink string) ([]*VoteState, error) { 299 | var resp []*VoteState 300 | if err := api.caller.Call("get_active_votes", []string{author, permlink}, &resp); err != nil { 301 | return nil, err 302 | } 303 | return resp, nil 304 | } 305 | 306 | func (api *API) GetAccountVotesRaw(voter string) (*json.RawMessage, error) { 307 | return call.Raw(api.caller, "get_account_votes", []string{voter}) 308 | } 309 | 310 | /* 311 | // Content 312 | (get_content) 313 | (get_content_replies) 314 | (get_discussions_by_author_before_date) - MISSING 315 | (get_replies_by_last_update) 316 | */ 317 | 318 | func (api *API) GetContentRaw(author, permlink string) (*json.RawMessage, error) { 319 | return call.Raw(api.caller, "get_content", []string{author, permlink}) 320 | } 321 | 322 | func (api *API) GetContent(author, permlink string) (*Content, error) { 323 | var resp Content 324 | if err := api.caller.Call("get_content", []string{author, permlink}, &resp); err != nil { 325 | return nil, err 326 | } 327 | return &resp, nil 328 | } 329 | 330 | func (api *API) GetContentRepliesRaw(parentAuthor, parentPermlink string) (*json.RawMessage, error) { 331 | return call.Raw(api.caller, "get_content_replies", []string{parentAuthor, parentPermlink}) 332 | } 333 | 334 | func (api *API) GetContentReplies(parentAuthor, parentPermlink string) ([]*Content, error) { 335 | var resp []*Content 336 | err := api.caller.Call("get_content_replies", []string{parentAuthor, parentPermlink}, &resp) 337 | if err != nil { 338 | return nil, err 339 | } 340 | return resp, nil 341 | } 342 | 343 | func (api *API) GetRepliesByLastUpdateRaw( 344 | startAuthor string, 345 | startPermlink string, 346 | limit uint32, 347 | ) (*json.RawMessage, error) { 348 | 349 | return call.Raw( 350 | api.caller, "get_replies_by_last_update", []interface{}{startAuthor, startPermlink, limit}) 351 | } 352 | 353 | /* 354 | // Witnesses 355 | (get_witnesses) 356 | (get_witness_by_account) 357 | (get_witnesses_by_vote) 358 | (lookup_witness_accounts) 359 | (get_witness_count) 360 | (get_active_witnesses) 361 | (get_miner_queue) 362 | */ 363 | 364 | // 365 | // Some randomly added functions. 366 | // 367 | 368 | func (api *API) GetOpsInBlockRaw(blockNum uint32, onlyVirtual bool) (*json.RawMessage, error) { 369 | return call.Raw( 370 | api.caller, "get_ops_in_block", []interface{}{blockNum, onlyVirtual}) 371 | } 372 | 373 | func (api *API) GetOpsInBlock(blockNum uint32, onlyVirtual bool) ([]*types.OperationObject, error) { 374 | var resp []*types.OperationObject 375 | err := api.caller.Call("get_ops_in_block", []interface{}{blockNum, onlyVirtual}, &resp) 376 | if err != nil { 377 | return nil, err 378 | } 379 | return resp, nil 380 | } 381 | -------------------------------------------------------------------------------- /types/operations.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | // Stdlib 5 | "encoding/json" 6 | 7 | // RPC 8 | "github.com/go-steem/rpc/encoding/transaction" 9 | ) 10 | 11 | // FC_REFLECT( steemit::chain::report_over_production_operation, 12 | // (reporter) 13 | // (first_block) 14 | // (second_block) ) 15 | 16 | type ReportOverProductionOperation struct { 17 | Reporter string `json:"reporter"` 18 | } 19 | 20 | func (op *ReportOverProductionOperation) Type() OpType { 21 | return TypeReportOverProduction 22 | } 23 | 24 | func (op *ReportOverProductionOperation) Data() interface{} { 25 | return op 26 | } 27 | 28 | // FC_REFLECT( steemit::chain::convert_operation, 29 | // (owner) 30 | // (requestid) 31 | // (amount) ) 32 | 33 | type ConvertOperation struct { 34 | Owner string `json:"owner"` 35 | RequestID uint32 `json:"requestid"` 36 | Amount string `json:"amount"` 37 | } 38 | 39 | func (op *ConvertOperation) Type() OpType { 40 | return TypeConvert 41 | } 42 | 43 | func (op *ConvertOperation) Data() interface{} { 44 | return op 45 | } 46 | 47 | // FC_REFLECT( steemit::chain::feed_publish_operation, 48 | // (publisher) 49 | // (exchange_rate) ) 50 | 51 | type FeedPublishOperation struct { 52 | Publisher string `json:"publisher"` 53 | ExchangeRate struct { 54 | Base string `json:"base"` 55 | Quote string `json:"quote"` 56 | } `json:"exchange_rate"` 57 | } 58 | 59 | func (op *FeedPublishOperation) Type() OpType { 60 | return TypeFeedPublish 61 | } 62 | 63 | func (op *FeedPublishOperation) Data() interface{} { 64 | return op 65 | } 66 | 67 | // FC_REFLECT( steemit::chain::pow, 68 | // (worker) 69 | // (input) 70 | // (signature) 71 | // (work) ) 72 | 73 | type POW struct { 74 | Worker string `json:"worker"` 75 | Input string `json:"input"` 76 | Signature string `json:"signature"` 77 | Work string `json:"work"` 78 | } 79 | 80 | // FC_REFLECT( steemit::chain::chain_properties, 81 | // (account_creation_fee) 82 | // (maximum_block_size) 83 | // (sbd_interest_rate) ); 84 | 85 | type ChainProperties struct { 86 | AccountCreationFee string `json:"account_creation_fee"` 87 | MaximumBlockSize uint32 `json:"maximum_block_size"` 88 | SBDInterestRate uint16 `json:"sbd_interest_rate"` 89 | } 90 | 91 | // FC_REFLECT( steemit::chain::pow_operation, 92 | // (worker_account) 93 | // (block_id) 94 | // (nonce) 95 | // (work) 96 | // (props) ) 97 | 98 | type POWOperation struct { 99 | WorkerAccount string `json:"worker_account"` 100 | BlockID string `json:"block_id"` 101 | Nonce *Int `json:"nonce"` 102 | Work *POW `json:"work"` 103 | Props *ChainProperties `json:"props"` 104 | } 105 | 106 | func (op *POWOperation) Type() OpType { 107 | return TypePOW 108 | } 109 | 110 | func (op *POWOperation) Data() interface{} { 111 | return op 112 | } 113 | 114 | // FC_REFLECT( steemit::chain::account_create_operation, 115 | // (fee) 116 | // (creator) 117 | // (new_account_name) 118 | // (owner) 119 | // (active) 120 | // (posting) 121 | // (memo_key) 122 | // (json_metadata) ) 123 | 124 | type AccountCreateOperation struct { 125 | Fee string `json:"fee"` 126 | Creator string `json:"creator"` 127 | NewAccountName string `json:"new_account_name"` 128 | Owner *Authority `json:"owner"` 129 | Active *Authority `json:"active"` 130 | Posting *Authority `json:"posting"` 131 | MemoKey string `json:"memo_key"` 132 | JsonMetadata string `json:"json_metadata"` 133 | } 134 | 135 | func (op *AccountCreateOperation) Type() OpType { 136 | return TypeAccountCreate 137 | } 138 | 139 | func (op *AccountCreateOperation) Data() interface{} { 140 | return op 141 | } 142 | 143 | // FC_REFLECT( steemit::chain::account_update_operation, 144 | // (account) 145 | // (owner) 146 | // (active) 147 | // (posting) 148 | // (memo_key) 149 | // (json_metadata) ) 150 | 151 | type AccountUpdateOperation struct { 152 | Account string `json:"account"` 153 | Owner *Authority `json:"owner"` 154 | Active *Authority `json:"active"` 155 | Posting *Authority `json:"posting"` 156 | MemoKey string `json:"memo_key"` 157 | JsonMetadata string `json:"json_metadata"` 158 | } 159 | 160 | func (op *AccountUpdateOperation) Type() OpType { 161 | return TypeAccountUpdate 162 | } 163 | 164 | func (op *AccountUpdateOperation) Data() interface{} { 165 | return op 166 | } 167 | 168 | // FC_REFLECT( steemit::chain::transfer_operation, 169 | // (from) 170 | // (to) 171 | // (amount) 172 | // (memo) ) 173 | 174 | type TransferOperation struct { 175 | From string `json:"from"` 176 | To string `json:"to"` 177 | Amount string `json:"amount"` 178 | Memo string `json:"memo"` 179 | } 180 | 181 | func (op *TransferOperation) Type() OpType { 182 | return TypeTransfer 183 | } 184 | 185 | func (op *TransferOperation) Data() interface{} { 186 | return op 187 | } 188 | 189 | // FC_REFLECT( steemit::chain::transfer_to_vesting_operation, 190 | // (from) 191 | // (to) 192 | // (amount) ) 193 | 194 | type TransferToVestingOperation struct { 195 | From string `json:"from"` 196 | To string `json:"to"` 197 | Amount string `json:"amount"` 198 | } 199 | 200 | func (op *TransferToVestingOperation) Type() OpType { 201 | return TypeTransferToVesting 202 | } 203 | 204 | func (op *TransferToVestingOperation) Data() interface{} { 205 | return op 206 | } 207 | 208 | // FC_REFLECT( steemit::chain::withdraw_vesting_operation, 209 | // (account) 210 | // (vesting_shares) ) 211 | 212 | type WithdrawVestingOperation struct { 213 | Account string `json:"account"` 214 | VestingShares string `json:"vesting_shares"` 215 | } 216 | 217 | func (op *WithdrawVestingOperation) Type() OpType { 218 | return TypeWithdrawVesting 219 | } 220 | 221 | func (op *WithdrawVestingOperation) Data() interface{} { 222 | return op 223 | } 224 | 225 | // FC_REFLECT( steemit::chain::set_withdraw_vesting_route_operation, 226 | // (from_account) 227 | // (to_account) 228 | // (percent) 229 | // (auto_vest) ) 230 | 231 | // FC_REFLECT( steemit::chain::witness_update_operation, 232 | // (owner) 233 | // (url) 234 | // (block_signing_key) 235 | // (props) 236 | // (fee) ) 237 | 238 | // FC_REFLECT( steemit::chain::account_witness_vote_operation, 239 | // (account) 240 | // (witness)(approve) ) 241 | 242 | type AccountWitnessVoteOperation struct { 243 | Account string `json:"account"` 244 | Witness string `json:"witness"` 245 | Approve bool `json:"approve"` 246 | } 247 | 248 | func (op *AccountWitnessVoteOperation) Type() OpType { 249 | return TypeAccountWitnessVote 250 | } 251 | 252 | func (op *AccountWitnessVoteOperation) Data() interface{} { 253 | return op 254 | } 255 | 256 | // FC_REFLECT( steemit::chain::account_witness_proxy_operation, 257 | // (account) 258 | // (proxy) ) 259 | 260 | type AccountWitnessProxyOperation struct { 261 | Account string `json:"account"` 262 | Proxy string `json:"proxy"` 263 | } 264 | 265 | func (op *AccountWitnessProxyOperation) Type() OpType { 266 | return TypeAccountWitnessProxy 267 | } 268 | 269 | func (op *AccountWitnessProxyOperation) Data() interface{} { 270 | return op 271 | } 272 | 273 | // FC_REFLECT( steemit::chain::comment_operation, 274 | // (parent_author) 275 | // (parent_permlink) 276 | // (author) 277 | // (permlink) 278 | // (title) 279 | // (body) 280 | // (json_metadata) ) 281 | 282 | // CommentOperation represents either a new post or a comment. 283 | // 284 | // In case Title is filled in and ParentAuthor is empty, it is a new post. 285 | // The post category can be read from ParentPermlink. 286 | type CommentOperation struct { 287 | Author string `json:"author"` 288 | Title string `json:"title"` 289 | Permlink string `json:"permlink"` 290 | ParentAuthor string `json:"parent_author"` 291 | ParentPermlink string `json:"parent_permlink"` 292 | Body string `json:"body"` 293 | JsonMetadata string `json:"json_metadata"` 294 | } 295 | 296 | func (op *CommentOperation) Type() OpType { 297 | return TypeComment 298 | } 299 | 300 | func (op *CommentOperation) Data() interface{} { 301 | return op 302 | } 303 | 304 | func (op *CommentOperation) IsStoryOperation() bool { 305 | return op.ParentAuthor == "" 306 | } 307 | 308 | // FC_REFLECT( steemit::chain::vote_operation, 309 | // (voter) 310 | // (author) 311 | // (permlink) 312 | // (weight) ) 313 | 314 | type VoteOperation struct { 315 | Voter string `json:"voter"` 316 | Author string `json:"author"` 317 | Permlink string `json:"permlink"` 318 | Weight Int16 `json:"weight"` 319 | } 320 | 321 | func (op *VoteOperation) Type() OpType { 322 | return TypeVote 323 | } 324 | 325 | func (op *VoteOperation) Data() interface{} { 326 | return op 327 | } 328 | 329 | func (op *VoteOperation) MarshalTransaction(encoder *transaction.Encoder) error { 330 | enc := transaction.NewRollingEncoder(encoder) 331 | enc.EncodeUVarint(uint64(TypeVote.Code())) 332 | enc.Encode(op.Voter) 333 | enc.Encode(op.Author) 334 | enc.Encode(op.Permlink) 335 | enc.Encode(op.Weight) 336 | return enc.Err() 337 | } 338 | 339 | // FC_REFLECT( steemit::chain::custom_operation, 340 | // (required_auths) 341 | // (id) 342 | // (data) ) 343 | 344 | // FC_REFLECT( steemit::chain::limit_order_create_operation, 345 | // (owner) 346 | // (orderid) 347 | // (amount_to_sell) 348 | // (min_to_receive) 349 | // (fill_or_kill) 350 | // (expiration) ) 351 | 352 | type LimitOrderCreateOperation struct { 353 | Owner string `json:"owner"` 354 | OrderID uint32 `json:"orderid"` 355 | AmountToSell string `json:"amount_to_sell"` 356 | MinToReceive string `json:"min_to_receive"` 357 | FillOrKill bool `json:"fill_or_kill"` 358 | Expiration *Time `json:"expiration"` 359 | } 360 | 361 | func (op *LimitOrderCreateOperation) Type() OpType { 362 | return TypeLimitOrderCreate 363 | } 364 | 365 | func (op *LimitOrderCreateOperation) Data() interface{} { 366 | return op 367 | } 368 | 369 | // FC_REFLECT( steemit::chain::limit_order_cancel_operation, 370 | // (owner) 371 | // (orderid) ) 372 | 373 | type LimitOrderCancelOperation struct { 374 | Owner string `json:"owner"` 375 | OrderID uint32 `json:"orderid"` 376 | } 377 | 378 | func (op *LimitOrderCancelOperation) Type() OpType { 379 | return TypeLimitOrderCancel 380 | } 381 | 382 | func (op *LimitOrderCancelOperation) Data() interface{} { 383 | return op 384 | } 385 | 386 | // FC_REFLECT( steemit::chain::delete_comment_operation, 387 | // (author) 388 | // (permlink) ) 389 | 390 | type DeleteCommentOperation struct { 391 | Author string `json:"author"` 392 | Permlink string `json:"permlink"` 393 | } 394 | 395 | func (op *DeleteCommentOperation) Type() OpType { 396 | return TypeDeleteComment 397 | } 398 | 399 | func (op *DeleteCommentOperation) Data() interface{} { 400 | return op 401 | } 402 | 403 | // FC_REFLECT( steemit::chain::comment_options_operation, 404 | // (author) 405 | // (permlink) 406 | // (max_accepted_payout) 407 | // (percent_steem_dollars) 408 | // (allow_votes) 409 | // (allow_curation_rewards) 410 | // (extensions) ) 411 | 412 | type CommentOptionsOperation struct { 413 | Author string `json:"author"` 414 | Permlink string `json:"permlink"` 415 | MaxAcceptedPayout string `json:"max_accepted_payout"` 416 | PercentSteemDollars uint16 `json:"percent_steem_dollars"` 417 | AllowVotes bool `json:"allow_votes"` 418 | AllowCurationRewards bool `json:"allow_curation_rewards"` 419 | Extensions []interface{} `json:"extensions"` 420 | } 421 | 422 | func (op *CommentOptionsOperation) Type() OpType { 423 | return TypeCommentOptions 424 | } 425 | 426 | func (op *CommentOptionsOperation) Data() interface{} { 427 | return op 428 | } 429 | 430 | type Authority struct { 431 | AccountAuths StringInt64Map `json:"account_auths"` 432 | KeyAuths StringInt64Map `json:"key_auths"` 433 | WeightThreshold uint32 `json:"weight_threshold"` 434 | } 435 | 436 | type UnknownOperation struct { 437 | kind OpType 438 | data *json.RawMessage 439 | } 440 | 441 | func (op *UnknownOperation) Type() OpType { 442 | return op.kind 443 | } 444 | 445 | func (op *UnknownOperation) Data() interface{} { 446 | return op.data 447 | } 448 | --------------------------------------------------------------------------------