Refactor HTTP clients

This commit is contained in:
Slavomir 2021-07-20 21:29:06 +02:00
parent 58f43f60bd
commit f0f399ab57
68 changed files with 2560 additions and 144 deletions

View File

@ -27,7 +27,7 @@ import (
func getClient() *rpc.Client {
httpHeaders := viper.GetStringSlice("global-http-header")
api := rpc.NewClient(sanitizeAPIURL(viper.GetString("global-rpc-url")))
api := rpc.New(sanitizeAPIURL(viper.GetString("global-rpc-url")))
for i := 0; i < 25; i++ {
if val := os.Getenv(fmt.Sprintf("SLNC_GLOBAL_HTTP_HEADER_%d", i)); val != "" {

4
go.mod
View File

@ -21,7 +21,7 @@ require (
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/magiconair/properties v1.8.1
github.com/mr-tron/base58 v1.2.0
github.com/onsi/gomega v1.10.1 // indirect
github.com/onsi/gomega v1.10.1
github.com/pkg/errors v0.9.1
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f
github.com/spf13/cobra v1.1.1
@ -30,9 +30,9 @@ require (
github.com/stretchr/testify v1.6.1
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 // indirect
github.com/tidwall/gjson v1.6.7
github.com/ybbus/jsonrpc v2.1.2+incompatible
go.opencensus.io v0.22.5 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/ratelimit v0.2.0
go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect

9
go.sum
View File

@ -37,6 +37,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@ -217,9 +219,11 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
@ -298,8 +302,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/ybbus/jsonrpc v2.1.2+incompatible h1:V4mkE9qhbDQ92/MLMIhlhMSbz8jNXdagC3xBR5NDwaQ=
github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@ -321,6 +323,8 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA=
go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
@ -559,6 +563,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -40,7 +40,7 @@ func TestDecoder_EventQueue_Diff(t *testing.T) {
newDataJSONFile := strings.ReplaceAll(newDataFile, ".bin.zst", ".json")
if os.Getenv("TESTDATA_UPDATE") == "true" {
client := rpc.NewClient("http://api.mainnet-beta.solana.com:80/rpc")
client := rpc.New("http://api.mainnet-beta.solana.com:80/rpc")
ctx := context.Background()
account := solana.MustPublicKeyFromBase58("13iGJcA4w5hcJZDjJbJQor1zUiDLE4jv2rMW9HkD5Eo1")

View File

@ -39,7 +39,7 @@ func TestFetchMarket(t *testing.T) {
//
client := rpc.NewClient(rpcURL)
client := rpc.New(rpcURL)
ctx := context.Background()
openOrderAdd, err := solana.PublicKeyFromBase58("jFoHUkNDC767PyK11cZM4zyNcpjLqFnSjaqEYp5GVBr")
@ -61,7 +61,7 @@ func TestStreamOpenOrders(t *testing.T) {
t.Skip("Setup 'RPC_URL' to run test i.e. 'ws://api.mainnet-beta.solana.com:80/rpc'")
return
}
client, err := ws.Dial(context.Background(), rpcURL)
client, err := ws.Connect(context.Background(), rpcURL)
require.NoError(t, err)
err = StreamOpenOrders(client)

View File

@ -68,7 +68,7 @@ func TestAccount(t *testing.T) {
func TestMint(t *testing.T) {
addr := solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
cli := rpc.NewClient("http://api.mainnet-beta.solana.com")
cli := rpc.New("http://api.mainnet-beta.solana.com")
var m Mint
err := cli.GetAccountDataIn(context.Background(), addr, &m)
@ -85,7 +85,7 @@ func TestMint(t *testing.T) {
func TestRawMint(t *testing.T) {
addr := solana.MustPublicKeyFromBase58("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v")
cli := rpc.NewClient("http://api.mainnet-beta.solana.com")
cli := rpc.New("http://api.mainnet-beta.solana.com")
resp, err := cli.GetAccountInfo(context.Background(), addr)
// handle `err`

View File

@ -15,10 +15,15 @@
package rpc
import (
"context"
"errors"
"net"
"net/http"
"time"
"github.com/ybbus/jsonrpc"
"github.com/gagliardetto/solana-go/rpc/jsonrpc"
"github.com/klauspost/compress/gzhttp"
"go.uber.org/ratelimit"
)
var ErrNotFound = errors.New("not found")
@ -26,29 +31,57 @@ var ErrNotConfirmed = errors.New("not confirmed")
type Client struct {
rpcURL string
rpcClient CallForClientInterface
rpcClient JSONRPCClient
headers http.Header
}
type CallForClientInterface interface {
CallFor(out interface{}, method string, params ...interface{}) error
type JSONRPCClient interface {
CallForInto(ctx context.Context, out interface{}, method string, params []interface{}) error
}
func NewClient(rpcURL string) *Client {
return NewClientWithOpts(rpcURL, nil)
// New creates a new Solana JSON RPC client.
func New(rpcEndpoint string) *Client {
opts := &jsonrpc.RPCClientOpts{
HTTPClient: newHTTP(),
}
rpcClient := jsonrpc.NewClientWithOpts(rpcEndpoint, opts)
return NewWithCustomRPCClient(rpcClient)
}
func NewClientWithOpts(rpcURL string, opts *jsonrpc.RPCClientOpts) *Client {
rpcClient := jsonrpc.NewClientWithOpts(rpcURL, opts)
// NewWithCustomRPCClient creates a new Solana RPC client
// with the provided RPC client.
func NewWithCustomRPCClient(rpcClient JSONRPCClient) *Client {
return &Client{
rpcURL: rpcURL,
rpcClient: rpcClient,
}
}
func NewWithCustomRPCClient(rpcClient CallForClientInterface) *Client {
return &Client{
rpcClient: rpcClient,
var _ JSONRPCClient = &clientWithRateLimiting{}
type clientWithRateLimiting struct {
rpcClient jsonrpc.RPCClient
rateLimiter ratelimit.Limiter
}
func (wr *clientWithRateLimiting) CallForInto(ctx context.Context, out interface{}, method string, params []interface{}) error {
wr.rateLimiter.Take()
return wr.rpcClient.CallForInto(ctx, &out, method, params)
}
// NewWithRateLimit creates a new rate-limitted Solana RPC client.
func NewWithRateLimit(
rpcEndpoint string,
rps int, // requests per second
) JSONRPCClient {
opts := &jsonrpc.RPCClientOpts{
HTTPClient: newHTTP(),
}
rpcClient := jsonrpc.NewClientWithOpts(rpcEndpoint, opts)
return &clientWithRateLimiting{
rpcClient: rpcClient,
rateLimiter: ratelimit.New(rps),
}
}
@ -58,3 +91,81 @@ func (c *Client) SetHeader(k, v string) {
}
c.headers.Set(k, v)
}
var (
defaultMaxIdleConnsPerHost = 9
defaultTimeout = 5 * time.Minute
defaultKeepAlive = 180 * time.Second
)
func newHTTPTransport() *http.Transport {
return &http.Transport{
IdleConnTimeout: defaultTimeout,
MaxConnsPerHost: defaultMaxIdleConnsPerHost,
MaxIdleConnsPerHost: defaultMaxIdleConnsPerHost,
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: defaultTimeout,
KeepAlive: defaultKeepAlive,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
// MaxIdleConns: 100,
TLSHandshakeTimeout: 10 * time.Second,
// ExpectContinueTimeout: 1 * time.Second,
}
}
// newHTTP returns a new Client from the provided config.
// Client is safe for concurrent use by multiple goroutines.
func newHTTP() *http.Client {
tr := newHTTPTransport()
return &http.Client{
Timeout: defaultTimeout,
Transport: gzhttp.Transport(tr),
}
}
// type wrapped struct {
// beforeHooks []JSONRPCClient
// cl JSONRPCClient
// afterHooks []JSONRPCClient
// }
// func (wr *wrapped) CallForInto(ctx context.Context, out interface{}, method string, params []interface{}) error {
// for _, ware := range wr.beforeHooks {
// err := ware.CallForInto(ctx, &out, method, params)
// if err != nil {
// return err
// }
// }
// err := wr.cl.CallForInto(ctx, &out, method, params)
// if err != nil {
// return err
// }
// for _, ware := range wr.afterHooks {
// err := ware.CallForInto(ctx, &out, method, params)
// if err != nil {
// return err
// }
// }
// return nil
// }
// func (cl *Client) WithBeforeHook(beforeHook JSONRPCClient) *Client {
// /* code */
// return nil
// }
// func (cl *Client) WithAfterHook(afterHook JSONRPCClient) *Client {
// /* code */
// return nil
// }
// // Expose this to make it easily expandable; or maybe don't ???
// func (cl *Client) CallForInto(ctx context.Context, out interface{}, method string, params []interface{}) error {
// return cl.rpcClient.CallForInto(ctx, &out, method, params)
// }

File diff suppressed because one or more lines are too long

View File

@ -79,7 +79,7 @@ func (cl *Client) GetConfirmedBlockWithOpts(
}
}
err = cl.rpcClient.CallFor(&out, "getConfirmedBlock", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getConfirmedBlock", params)
return
}
@ -106,7 +106,7 @@ func (cl *Client) GetConfirmedBlocks(
params = append(params, M{"commitment": string(commitment)})
}
err = cl.rpcClient.CallFor(&out, "getConfirmedBlocks", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getConfirmedBlocks", params)
return
}
@ -126,7 +126,7 @@ func (cl *Client) GetConfirmedBlocksWithLimit(
params = append(params, M{"commitment": string(commitment)})
}
err = cl.rpcClient.CallFor(&out, "getConfirmedBlocksWithLimit", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getConfirmedBlocksWithLimit", params)
return
}
@ -143,7 +143,7 @@ func (cl *Client) GetConfirmedSignaturesForAddress2(
params := []interface{}{address, opts}
err = cl.rpcClient.CallFor(&out, "getConfirmedSignaturesForAddress2", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getConfirmedSignaturesForAddress2", params)
return
}
@ -154,7 +154,7 @@ func (cl *Client) GetConfirmedTransaction(
) (out *TransactionWithMeta, err error) {
params := []interface{}{signature, "json"}
err = cl.rpcClient.CallFor(&out, "getConfirmedTransaction", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getConfirmedTransaction", params)
if err != nil {
return nil, err
}

View File

@ -105,7 +105,7 @@ func (cl *Client) GetAccountInfoWithOpts(
params = append(params, obj)
}
err = cl.rpcClient.CallFor(&out, "getAccountInfo", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getAccountInfo", params)
if err != nil {
return nil, err
}

View File

@ -34,6 +34,6 @@ func (cl *Client) GetBalance(
params = append(params, M{"commitment": string(commitment)})
}
err = cl.rpcClient.CallFor(&out, "getBalance", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBalance", params)
return
}

View File

@ -90,7 +90,7 @@ func (cl *Client) GetBlockWithOpts(
params := []interface{}{slot, obj}
err = cl.rpcClient.CallFor(&out, "getBlock", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlock", params)
if err != nil {
return nil, err

View File

@ -12,7 +12,7 @@ func (cl *Client) GetBlockCommitment(
block uint64, // block, identified by Slot
) (out *GetBlockCommitmentResult, err error) {
params := []interface{}{block}
err = cl.rpcClient.CallFor(&out, "getBlockCommitment", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlockCommitment", params)
return
}

View File

@ -13,6 +13,6 @@ func (cl *Client) GetBlockHeight(
if commitment != "" {
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getBlockHeight", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlockHeight", params)
return
}

View File

@ -77,7 +77,7 @@ func (cl *Client) GetBlockProductionWithOpts(
params = append(params, obj)
}
}
err = cl.rpcClient.CallFor(&out, "getBlockProduction", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlockProduction", params)
return
}

View File

@ -20,6 +20,6 @@ func (cl *Client) GetBlockTime(
block uint64, // block, identified by Slot
) (out *UnixTimeSeconds, err error) {
params := []interface{}{block}
err = cl.rpcClient.CallFor(&out, "getBlockTime", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlockTime", params)
return
}

View File

@ -29,7 +29,7 @@ func (cl *Client) GetBlocks(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getBlocks", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlocks", params)
return
}

View File

@ -23,6 +23,6 @@ func (cl *Client) GetBlocksWithLimit(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getBlocksWithLimit", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getBlocksWithLimit", params)
return
}

View File

@ -9,7 +9,7 @@ import (
// GetClusterNodes returns information about all the nodes participating in the cluster.
func (cl *Client) GetClusterNodes(ctx context.Context) (out []*GetClusterNodesResult, err error) {
err = cl.rpcClient.CallFor(&out, "getClusterNodes")
err = cl.rpcClient.CallForInto(ctx, &out, "getClusterNodes", nil)
return
}

View File

@ -15,7 +15,7 @@ func (cl *Client) GetEpochInfo(
if commitment != "" {
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getEpochInfo", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getEpochInfo", params)
return
}

View File

@ -8,7 +8,7 @@ import (
// GetEpochSchedule returns epoch schedule information from this cluster's genesis config.
func (cl *Client) GetEpochSchedule(ctx context.Context) (out *GetEpochScheduleResult, err error) {
err = cl.rpcClient.CallFor(&out, "getEpochSchedule")
err = cl.rpcClient.CallForInto(ctx, &out, "getEpochSchedule", nil)
return
}

View File

@ -17,7 +17,7 @@ func (cl *Client) GetFeeCalculatorForBlockhash(
if commitment != "" {
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getFeeCalculatorForBlockhash", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getFeeCalculatorForBlockhash", params)
return
}

View File

@ -8,7 +8,7 @@ import (
// GetFeeRateGovernor returns the fee rate governor information from the root bank.
func (cl *Client) GetFeeRateGovernor(ctx context.Context) (out *GetFeeRateGovernorResult, err error) {
err = cl.rpcClient.CallFor(&out, "getFeeRateGovernor")
err = cl.rpcClient.CallForInto(ctx, &out, "getFeeRateGovernor", nil)
return
}

View File

@ -19,7 +19,7 @@ func (cl *Client) GetFees(
if commitment != "" {
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getFees", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getFees", params)
return
}

View File

@ -6,6 +6,6 @@ import (
// GetFirstAvailableBlock returns the slot of the lowest confirmed block that has not been purged from the ledger.
func (cl *Client) GetFirstAvailableBlock(ctx context.Context) (out uint64, err error) {
err = cl.rpcClient.CallFor(&out, "getFirstAvailableBlock")
err = cl.rpcClient.CallForInto(ctx, &out, "getFirstAvailableBlock", nil)
return
}

View File

@ -8,6 +8,6 @@ import (
// GetGenesisHash returns the genesis hash.
func (cl *Client) GetGenesisHash(ctx context.Context) (out solana.Hash, err error) {
err = cl.rpcClient.CallFor(&out, "getGenesisHash")
err = cl.rpcClient.CallForInto(ctx, &out, "getGenesisHash", nil)
return
}

View File

@ -15,7 +15,7 @@ import (
// - If the node is unhealthy, a JSON RPC error response is returned.
// The specifics of the error response are UNSTABLE and may change in the future.
func (cl *Client) GetHealth(ctx context.Context) (out string, err error) {
err = cl.rpcClient.CallFor(&out, "getHealth")
err = cl.rpcClient.CallForInto(ctx, &out, "getHealth", nil)
return
}

View File

@ -8,7 +8,7 @@ import (
// GetIdentity returns the identity pubkey for the current node.
func (cl *Client) GetIdentity(ctx context.Context) (out *GetIdentityResult, err error) {
err = cl.rpcClient.CallFor(&out, "getIdentity")
err = cl.rpcClient.CallForInto(ctx, &out, "getIdentity", nil)
return
}

View File

@ -17,7 +17,7 @@ func (cl *Client) GetInflationGovernor(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getInflationGovernor", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getInflationGovernor", params)
return
}

View File

@ -8,7 +8,7 @@ import (
// GetInflationRate returns the specific inflation values for the current epoch.
func (cl *Client) GetInflationRate(ctx context.Context) (out *GetInflationRateResult, err error) {
err = cl.rpcClient.CallFor(&out, "getInflationRate")
err = cl.rpcClient.CallForInto(ctx, &out, "getInflationRate", nil)
return
}

View File

@ -39,7 +39,7 @@ func (cl *Client) GetInflationReward(
}
}
// TODO: check
err = cl.rpcClient.CallFor(&out, "getInflationReward", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getInflationReward", params)
return
}

View File

@ -32,7 +32,7 @@ func (cl *Client) GetLargestAccounts(
if len(obj) > 0 {
params = append(params, obj)
}
err = cl.rpcClient.CallFor(&out, "getLargestAccounts", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getLargestAccounts", params)
return
}

View File

@ -50,7 +50,7 @@ func (cl *Client) GetLeaderScheduleWithOpts(
params = append(params, obj)
}
}
err = cl.rpcClient.CallFor(&out, "getLeaderSchedule", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getLeaderSchedule", params)
if err != nil {
return nil, err
}

View File

@ -6,6 +6,6 @@ import (
// GetMaxRetransmitSlot returns the max slot seen from retransmit stage.
func (cl *Client) GetMaxRetransmitSlot(ctx context.Context) (out uint64, err error) {
err = cl.rpcClient.CallFor(&out, "getMaxRetransmitSlot")
err = cl.rpcClient.CallForInto(ctx, &out, "getMaxRetransmitSlot", nil)
return
}

View File

@ -8,6 +8,6 @@ type GetMaxShredInsertSlotResult struct{}
// GetMaxShredInsertSlot returns the max slot seen from after shred insert.
func (cl *Client) GetMaxShredInsertSlot(ctx context.Context) (out uint64, err error) {
err = cl.rpcClient.CallFor(&out, "getMaxShredInsertSlot")
err = cl.rpcClient.CallForInto(ctx, &out, "getMaxShredInsertSlot", nil)
return
}

View File

@ -27,6 +27,6 @@ func (cl *Client) GetMinimumBalanceForRentExemption(
if commitment != "" {
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&lamport, "getMinimumBalanceForRentExemption", params)
err = cl.rpcClient.CallForInto(ctx, &lamport, "getMinimumBalanceForRentExemption", params)
return
}

View File

@ -54,7 +54,7 @@ func (cl *Client) GetMultipleAccountsWithOpts(
}
}
err = cl.rpcClient.CallFor(&out, "getMultipleAccounts", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getMultipleAccounts", params)
if err != nil {
return nil, err
}

View File

@ -60,6 +60,6 @@ func (cl *Client) GetProgramAccountsWithOpts(
params := []interface{}{publicKey, obj}
err = cl.rpcClient.CallFor(&out, "getProgramAccounts", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getProgramAccounts", params)
return
}

View File

@ -28,6 +28,6 @@ func (cl *Client) GetRecentBlockhash(
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getRecentBlockhash", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getRecentBlockhash", params)
return
}

View File

@ -14,7 +14,7 @@ func (cl *Client) GetRecentPerformanceSamples(ctx context.Context, limit *int) (
if limit != nil {
params = append(params, limit)
}
err = cl.rpcClient.CallFor(&out, "getRecentPerformanceSamples", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getRecentPerformanceSamples", params)
return
}

View File

@ -25,7 +25,7 @@ func (cl *Client) GetSignatureStatuses(
if searchTransactionHistory {
params = append(params, M{"searchTransactionHistory": searchTransactionHistory})
}
err = cl.rpcClient.CallFor(&out, "getSignatureStatuses", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getSignatureStatuses", params)
if err != nil {
return nil, err
}

View File

@ -70,6 +70,6 @@ func (cl *Client) GetSignaturesForAddressWithOpts(
}
}
err = cl.rpcClient.CallFor(&out, "getSignaturesForAddress", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getSignaturesForAddress", params)
return
}

View File

@ -27,6 +27,6 @@ func (cl *Client) GetSlot(
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getSlot", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getSlot", params)
return
}

View File

@ -15,6 +15,6 @@ func (cl *Client) GetSlotLeader(
if commitment != "" {
params = append(params, M{"commitment": commitment})
}
err = cl.rpcClient.CallFor(&out, "getSlotLeader", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getSlotLeader", params)
return
}

View File

@ -13,6 +13,6 @@ func (cl *Client) GetSlotLeaders(
limit uint64,
) (out []solana.PublicKey, err error) {
params := []interface{}{start, limit}
err = cl.rpcClient.CallFor(&out, "getSlotLeaders", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getSlotLeaders", params)
return
}

View File

@ -6,6 +6,6 @@ import (
// GetSnapshotSlot returns the highest slot that the node has a snapshot for.
func (cl *Client) GetSnapshotSlot(ctx context.Context) (out uint64, err error) {
err = cl.rpcClient.CallFor(&out, "getSnapshotSlot")
err = cl.rpcClient.CallForInto(ctx, &out, "getSnapshotSlot", nil)
return
}

View File

@ -32,7 +32,7 @@ func (cl *Client) GetStakeActivation(
params = append(params, obj)
}
}
err = cl.rpcClient.CallFor(&out, "getStakeActivation", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getStakeActivation", params)
return
}

View File

@ -17,7 +17,7 @@ func (cl *Client) GetSupply(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getSupply", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getSupply", params)
return
}

View File

@ -18,7 +18,7 @@ func (cl *Client) GetTokenAccountBalance(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getTokenAccountBalance", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTokenAccountBalance", params)
return
}

View File

@ -73,7 +73,7 @@ func (cl *Client) GetTokenAccountsByDelegate(
}
}
err = cl.rpcClient.CallFor(&out, "getTokenAccountsByDelegate", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTokenAccountsByDelegate", params)
return
}

View File

@ -58,6 +58,6 @@ func (cl *Client) GetTokenAccountsByOwner(
}
}
err = cl.rpcClient.CallFor(&out, "getTokenAccountsByOwner", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTokenAccountsByOwner", params)
return
}

View File

@ -18,7 +18,7 @@ func (cl *Client) GetTokenLargestAccounts(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getTokenLargestAccounts", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTokenLargestAccounts", params)
return
}

View File

@ -18,7 +18,7 @@ func (cl *Client) GetTokenSupply(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getTokenSupply", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTokenSupply", params)
return
}

View File

@ -36,7 +36,7 @@ func (cl *Client) GetTransaction(
params = append(params, obj)
}
}
err = cl.rpcClient.CallFor(&out, "getTransaction", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTransaction", params)
if err != nil {
return nil, err
}

View File

@ -15,6 +15,6 @@ func (cl *Client) GetTransactionCount(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&out, "getTransactionCount", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getTransactionCount", params)
return
}

View File

@ -8,7 +8,7 @@ import (
// GetVersion returns the current solana versions running on the node.
func (cl *Client) GetVersion(ctx context.Context) (out *GetVersionResult, err error) {
err = cl.rpcClient.CallFor(&out, "getVersion")
err = cl.rpcClient.CallForInto(ctx, &out, "getVersion", nil)
return
}

View File

@ -47,7 +47,7 @@ func (cl *Client) GetVoteAccounts(
params = append(params, obj)
}
}
err = cl.rpcClient.CallFor(&out, "getVoteAccounts", params)
err = cl.rpcClient.CallForInto(ctx, &out, "getVoteAccounts", params)
return
}

21
rpc/jsonrpc/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Alexander Gehres
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

472
rpc/jsonrpc/README.md Normal file
View File

@ -0,0 +1,472 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/ybbus/jsonrpc)](https://goreportcard.com/report/github.com/ybbus/jsonrpc)
[![GoDoc](https://godoc.org/github.com/ybbus/jsonrpc?status.svg)](https://godoc.org/github.com/ybbus/jsonrpc)
[![GitHub license](https://img.shields.io/github/license/mashape/apistatus.svg)]()
# JSON-RPC 2.0 Client for golang
A go implementation of an rpc client using json as data format over http.
The implementation is based on the JSON-RPC 2.0 specification: http://www.jsonrpc.org/specification
Supports:
- requests with arbitrary parameters
- convenient response retrieval
- batch requests
- custom http client (e.g. proxy, tls config)
- custom headers (e.g. basic auth)
## Installation
```sh
go get -u github.com/ybbus/jsonrpc
```
## Getting started
Let's say we want to retrieve a person struct with a specific id using rpc-json over http.
Then we want to save this person after we changed a property.
(Error handling is omitted here)
```go
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
var person *Person
rpcClient.CallFor(&person, "getPersonById", 4711)
person.Age = 33
rpcClient.Call("updatePerson", person)
}
```
## In detail
### Generating rpc-json requests
Let's start by executing a simple json-rpc http call:
In production code: Always make sure to check err != nil first!
This calls generate and send a valid rpc-json object. (see: http://www.jsonrpc.org/specification#request_object)
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
rpcClient.Call("getDate")
// generates body: {"method":"getDate","id":1,"jsonrpc":"2.0"}
}
```
Call a function with parameter:
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
rpcClient.Call("addNumbers", 1, 2)
// generates body: {"method":"addNumbers","params":[1,2],"id":1,"jsonrpc":"2.0"}
}
```
Call a function with arbitrary parameters:
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
rpcClient.Call("createPerson", "Alex", 33, "Germany")
// generates body: {"method":"createPerson","params":["Alex",33,"Germany"],"id":1,"jsonrpc":"2.0"}
}
```
Call a function providing custom data structures as parameters:
```go
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Country string `json:"country"`
}
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
rpcClient.Call("createPerson", &Person{"Alex", 33, "Germany"})
// generates body: {"jsonrpc":"2.0","method":"createPerson","params":{"name":"Alex","age":33,"country":"Germany"},"id":1}
}
```
Complex example:
```go
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Country string `json:"country"`
}
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
rpcClient.Call("createPersonsWithRole", &Person{"Alex", 33, "Germany"}, &Person{"Barney", 38, "Germany"}, []string{"Admin", "User"})
// generates body: {"jsonrpc":"2.0","method":"createPersonsWithRole","params":[{"name":"Alex","age":33,"country":"Germany"},{"name":"Barney","age":38,"country":"Germany"},["Admin","User"]],"id":1}
}
```
Some examples and resulting JSON-RPC objects:
```go
rpcClient.Call("missingParam")
{"method":"missingParam"}
rpcClient.Call("nullParam", nil)
{"method":"nullParam","params":[null]}
rpcClient.Call("boolParam", true)
{"method":"boolParam","params":[true]}
rpcClient.Call("boolParams", true, false, true)
{"method":"boolParams","params":[true,false,true]}
rpcClient.Call("stringParam", "Alex")
{"method":"stringParam","params":["Alex"]}
rpcClient.Call("stringParams", "JSON", "RPC")
{"method":"stringParams","params":["JSON","RPC"]}
rpcClient.Call("numberParam", 123)
{"method":"numberParam","params":[123]}
rpcClient.Call("numberParams", 123, 321)
{"method":"numberParams","params":[123,321]}
rpcClient.Call("floatParam", 1.23)
{"method":"floatParam","params":[1.23]}
rpcClient.Call("floatParams", 1.23, 3.21)
{"method":"floatParams","params":[1.23,3.21]}
rpcClient.Call("manyParams", "Alex", 35, true, nil, 2.34)
{"method":"manyParams","params":["Alex",35,true,null,2.34]}
rpcClient.Call("singlePointerToStruct", &person)
{"method":"singlePointerToStruct","params":{"name":"Alex","age":35,"country":"Germany"}}
rpcClient.Call("multipleStructs", &person, &drink)
{"method":"multipleStructs","params":[{"name":"Alex","age":35,"country":"Germany"},{"name":"Cuba Libre","ingredients":["rum","cola"]}]}
rpcClient.Call("singleStructInArray", []*Person{&person})
{"method":"singleStructInArray","params":[{"name":"Alex","age":35,"country":"Germany"}]}
rpcClient.Call("namedParameters", map[string]interface{}{
"name": "Alex",
"age": 35,
})
{"method":"namedParameters","params":{"age":35,"name":"Alex"}}
rpcClient.Call("anonymousStruct", struct {
Name string `json:"name"`
Age int `json:"age"`
}{"Alex", 33})
{"method":"anonymousStructWithTags","params":{"name":"Alex","age":33}}
rpcClient.Call("structWithNullField", struct {
Name string `json:"name"`
Address *string `json:"address"`
}{"Alex", nil})
{"method":"structWithNullField","params":{"name":"Alex","address":null}}
```
### Working with rpc-json responses
Before working with the response object, make sure to check err != nil.
Also keep in mind that the json-rpc result field can be nil even on success.
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, err := rpcClient.Call("addNumbers", 1, 2)
if err != nil {
// error handling goes here e.g. network / http error
}
}
```
If an http error occurred, maybe you are interested in the error code (403 etc.)
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, err := rpcClient.Call("addNumbers", 1, 2)
switch e := err.(type) {
case nil: // if error is nil, do nothing
case *HTTPError:
// use e.Code here
return
default:
// any other error
return
}
// no error, go on...
}
```
The next thing you have to check is if an rpc-json protocol error occurred. This is done by checking if the Error field in the rpc-response != nil:
(see: http://www.jsonrpc.org/specification#error_object)
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, err := rpcClient.Call("addNumbers", 1, 2)
if err != nil {
//error handling goes here
}
if response.Error != nil {
// rpc error handling goes here
// check response.Error.Code, response.Error.Message and optional response.Error.Data
}
}
```
After making sure that no errors occurred you can now examine the RPCResponse object.
When executing a json-rpc request, most of the time you will be interested in the "result"-property of the returned json-rpc response object.
(see: http://www.jsonrpc.org/specification#response_object)
The library provides some helper functions to retrieve the result in the data format you are interested in.
Again: check for err != nil here to be sure the expected type was provided in the response and could be parsed.
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, _ := rpcClient.Call("addNumbers", 1, 2)
result, err := response.GetInt()
if err != nil {
// result cannot be unmarshalled as integer
}
// helpers provided for all primitive types:
response.GetInt()
response.GetFloat()
response.GetString()
response.GetBool()
}
```
Retrieving arrays and objects is also very simple:
```go
// json annotations are only required to transform the structure back to json
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, _ := rpcClient.Call("getPersonById", 123)
var person *Person
err := response.GetObject(&person) // expects a rpc-object result value like: {"id": 123, "name": "alex", "age": 33}
if err != nil || person == nil {
// some error on json unmarshal level or json result field was null
}
fmt.Println(person.Name)
// we can also set default values if they are missing from the result, or result == null:
person2 := &Person{
Id: 0,
Name: "<empty>",
Age: -1,
}
err := response.GetObject(&person2) // expects a rpc-object result value like: {"id": 123, "name": "alex", "age": 33}
if err != nil || person2 == nil {
// some error on json unmarshal level or json result field was null
}
fmt.Println(person2.Name) // prints "<empty>" if "name" field was missing in result-json
}
```
Retrieving arrays:
```go
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, _ := rpcClient.Call("getRandomNumbers", 10)
rndNumbers := []int{}
err := response.GetObject(&rndNumbers) // expects a rpc-object result value like: [10, 188, 14, 3]
if err != nil {
// do error handling
}
for _, num := range rndNumbers {
fmt.Printf("%v\n", num)
}
}
```
### Using convenient function CallFor()
A very handy way to quickly invoke methods and retrieve results is by using CallFor()
You can directly provide an object where the result should be stored. Be sure to provide it be reference.
An error is returned if:
- there was an network / http error
- RPCError object is not nil (err can be casted to this object)
- rpc result could not be parsed into provided object
One of te above examples could look like this:
```go
// json annotations are only required to transform the structure back to json
type Person struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
var person *Person
err := rpcClient.CallFor(&person, "getPersonById", 123)
if err != nil || person == nil {
// handle error
}
fmt.Println(person.Name)
}
```
Most of the time it is ok to check if a struct field is 0, empty string "" etc. to check if it was provided by the json rpc response.
But if you want to be sure that a JSON-RPC response field was missing or not, you should use pointers to the fields.
This is just a single example since all this Unmarshaling is standard go json functionality, exactly as if you would call json.Unmarshal(rpcResponse.ResultAsByteArray, &objectToStoreResult)
```
type Person struct {
Id *int `json:"id"`
Name *string `json:"name"`
Age *int `json:"age"`
}
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
var person *Person
err := rpcClient.CallFor(&person, "getPersonById", 123)
if err != nil || person == nil {
// handle error
}
if person.Name == nil {
// json rpc response did not provide a field "name" in the result object
}
}
```
### Using RPC Batch Requests
You can send multiple RPC-Requests in one single HTTP request using RPC Batch Requests.
```
func main() {
rpcClient := jsonrpc.NewClient("http://my-rpc-service:8080/rpc")
response, _ := rpcClient.CallBatch(RPCRequests{
NewRequest("myMethod1", 1, 2, 3),
NewRequest("anotherMethod", "Alex", 35, true),
NewRequest("myMethod2", &Person{
Name: "Emmy",
Age: 4,
}),
})
}
```
Keep the following in mind:
- the request / response id's are important to map the requests to the responses. CallBatch() automatically sets the ids to requests[i].ID == i
- the response can be provided in an unordered and maybe incomplete form
- when you want to set the id yourself use, CallRaw()
There are some helper methods for batch request results:
```
func main() {
// [...]
result.HasErrors() // returns true if one of the rpc response objects has Error field != nil
resultMap := result.AsMap() // returns a map for easier retrieval of requests
if response123, ok := resultMap[123]; ok {
// response object with id 123 exists, use it here
// response123.ID == 123
response123.GetObjectAs(&person)
// ...
}
}
```
### Raw functions
There are also Raw function calls. Consider the non Raw functions first, unless you know what you are doing.
You can create invalid json rpc requests and have to take care of id's etc. yourself.
Also check documentation of Params() for raw requests.
### Custom Headers, Basic authentication
If the rpc-service is running behind a basic authentication you can easily set the Authorization header:
```go
func main() {
rpcClient := jsonrpc.NewClientWithOpts("http://my-rpc-service:8080/rpc", &jsonrpc.RPCClientOpts{
CustomHeaders: map[string]string{
"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("myUser"+":"+"mySecret")),
},
})
response, _ := rpcClient.Call("addNumbers", 1, 2) // send with Authorization-Header
}
```
### Using oauth
Using oauth is also easy, e.g. with clientID and clientSecret authentication
```go
func main() {
credentials := clientcredentials.Config{
ClientID: "myID",
ClientSecret: "mySecret",
TokenURL: "http://mytokenurl",
}
rpcClient := jsonrpc.NewClientWithOpts("http://my-rpc-service:8080/rpc", &jsonrpc.RPCClientOpts{
HTTPClient: credentials.Client(context.Background()),
})
// requests now retrieve and use an oauth token
}
```
### Set a custom httpClient
If you have some special needs on the http.Client of the standard go library, just provide your own one.
For example to use a proxy when executing json-rpc calls:
```go
func main() {
proxyURL, _ := url.Parse("http://proxy:8080")
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
httpClient := &http.Client{
Transport: transport,
}
rpcClient := jsonrpc.NewClientWithOpts("http://my-rpc-service:8080/rpc", &jsonrpc.RPCClientOpts{
HTTPClient: httpClient,
})
// requests now use proxy
}
```

651
rpc/jsonrpc/jsonrpc.go Normal file
View File

@ -0,0 +1,651 @@
// Package jsonrpc provides a JSON-RPC 2.0 client that sends JSON-RPC requests and receives JSON-RPC responses using HTTP.
package jsonrpc
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
)
const (
jsonrpcVersion = "2.0"
)
// RPCClient sends JSON-RPC requests over HTTP to the provided JSON-RPC backend.
//
// RPCClient is created using the factory function NewClient().
type RPCClient interface {
// Call is used to send a JSON-RPC request to the server endpoint.
//
// The spec states, that params can only be an array or an object, no primitive values.
// So there are a few simple rules to notice:
//
// 1. no params: params field is omitted. e.g. Call("getinfo")
//
// 2. single params primitive value: value is wrapped in array. e.g. Call("getByID", 1423)
//
// 3. single params value array or object: value is unchanged. e.g. Call("storePerson", &Person{Name: "Alex"})
//
// 4. multiple params values: always wrapped in array. e.g. Call("setDetails", "Alex, 35, "Germany", true)
//
// Examples:
// Call("getinfo") -> {"method": "getinfo"}
// Call("getPersonId", 123) -> {"method": "getPersonId", "params": [123]}
// Call("setName", "Alex") -> {"method": "setName", "params": ["Alex"]}
// Call("setMale", true) -> {"method": "setMale", "params": [true]}
// Call("setNumbers", []int{1, 2, 3}) -> {"method": "setNumbers", "params": [1, 2, 3]}
// Call("setNumbers", 1, 2, 3) -> {"method": "setNumbers", "params": [1, 2, 3]}
// Call("savePerson", &Person{Name: "Alex", Age: 35}) -> {"method": "savePerson", "params": {"name": "Alex", "age": 35}}
// Call("setPersonDetails", "Alex", 35, "Germany") -> {"method": "setPersonDetails", "params": ["Alex", 35, "Germany"}}
//
// for more information, see the examples or the unit tests
Call(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error)
// CallRaw is like Call() but without magic in the requests.Params field.
// The RPCRequest object is sent exactly as you provide it.
// See docs: NewRequest, RPCRequest, Params()
//
// It is recommended to first consider Call() and CallFor()
CallRaw(ctx context.Context, request *RPCRequest) (*RPCResponse, error)
// CallFor is a very handy function to send a JSON-RPC request to the server endpoint
// and directly specify an object to store the response.
//
// out: will store the unmarshaled object, if request was successful.
// should always be provided by references. can be nil even on success.
// the behaviour is the same as expected from json.Unmarshal()
//
// method and params: see Call() function
//
// if the request was not successful (network, http error) or the rpc response returns an error,
// an error is returned. if it was an JSON-RPC error it can be casted
// to *RPCError.
//
CallFor(ctx context.Context, out interface{}, method string, params ...interface{}) error
CallForInto(ctx context.Context, out interface{}, method string, params []interface{}) error
// CallBatch invokes a list of RPCRequests in a single batch request.
//
// Most convenient is to use the following form:
// CallBatch(RPCRequests{
// Batch("myMethod1", 1, 2, 3),
// Batch("myMethod2), "Test"),
// })
//
// You can create the []*RPCRequest array yourself, but it is not recommended and you should notice the following:
// - field Params is sent as provided, so Params: 2 forms an invalid json (correct would be Params: []int{2})
// - you can use the helper function Params(1, 2, 3) to use the same format as in Call()
// - field JSONRPC is overwritten and set to value: "2.0"
// - field ID is overwritten and set incrementally and maps to the array position (e.g. requests[5].ID == 5)
//
//
// Returns RPCResponses that is of type []*RPCResponse
// - note that a list of RPCResponses can be received unordered so it can happen that: responses[i] != responses[i].ID
// - RPCPersponses is enriched with helper functions e.g.: responses.HasError() returns true if one of the responses holds an RPCError
CallBatch(ctx context.Context, requests RPCRequests) (RPCResponses, error)
// CallBatchRaw invokes a list of RPCRequests in a single batch request.
// It sends the RPCRequests parameter is it passed (no magic, no id autoincrement).
//
// Consider to use CallBatch() instead except you have some good reason not to.
//
// CallBatchRaw(RPCRequests{
// &RPCRequest{
// ID: 123, // this won't be replaced in CallBatchRaw
// JSONRPC: "wrong", // this won't be replaced in CallBatchRaw
// Method: "myMethod1",
// Params: []int{1}, // there is no magic, be sure to only use array or object
// },
// &RPCRequest{
// ID: 612,
// JSONRPC: "2.0",
// Method: "myMethod2",
// Params: Params("Alex", 35, true), // you can use helper function Params() (see doc)
// },
// })
//
// Returns RPCResponses that is of type []*RPCResponse
// - note that a list of RPCResponses can be received unordered
// - the id's must be mapped against the id's you provided
// - RPCPersponses is enriched with helper functions e.g.: responses.HasError() returns true if one of the responses holds an RPCError
CallBatchRaw(ctx context.Context, requests RPCRequests) (RPCResponses, error)
}
// RPCRequest represents a JSON-RPC request object.
//
// Method: string containing the method to be invoked
//
// Params: can be nil. if not must be an json array or object
//
// ID: may always set to 1 for single requests. Should be unique for every request in one batch request.
//
// JSONRPC: must always be set to "2.0" for JSON-RPC version 2.0
//
// See: http://www.jsonrpc.org/specification#request_object
//
// Most of the time you shouldn't create the RPCRequest object yourself.
// The following functions do that for you:
// Call(), CallFor(), NewRequest()
//
// If you want to create it yourself (e.g. in batch or CallRaw()), consider using Params().
// Params() is a helper function that uses the same parameter syntax as Call().
//
// e.g. to manually create an RPCRequest object:
// request := &RPCRequest{
// Method: "myMethod",
// Params: Params("Alex", 35, true),
// }
//
// If you know what you are doing you can omit the Params() call to avoid some reflection but potentially create incorrect rpc requests:
//request := &RPCRequest{
// Method: "myMethod",
// Params: 2, <-- invalid since a single primitive value must be wrapped in an array --> no magic without Params()
// }
//
// correct:
// request := &RPCRequest{
// Method: "myMethod",
// Params: []int{2}, <-- invalid since a single primitive value must be wrapped in an array
// }
type RPCRequest struct {
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
ID int `json:"id"`
JSONRPC string `json:"jsonrpc"`
}
// NewRequest returns a new RPCRequest that can be created using the same convenient parameter syntax as Call()
//
// e.g. NewRequest("myMethod", "Alex", 35, true)
func NewRequest(method string, params ...interface{}) *RPCRequest {
request := &RPCRequest{
Method: method,
Params: Params(params...),
JSONRPC: jsonrpcVersion,
}
return request
}
// RPCResponse represents a JSON-RPC response object.
//
// Result: holds the result of the rpc call if no error occurred, nil otherwise. can be nil even on success.
//
// Error: holds an RPCError object if an error occurred. must be nil on success.
//
// ID: may always be 0 for single requests. is unique for each request in a batch call (see CallBatch())
//
// JSONRPC: must always be set to "2.0" for JSON-RPC version 2.0
//
// See: http://www.jsonrpc.org/specification#response_object
type RPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
ID int `json:"id"`
}
// RPCError represents a JSON-RPC error object if an RPC error occurred.
//
// Code: holds the error code
//
// Message: holds a short error message
//
// Data: holds additional error data, may be nil
//
// See: http://www.jsonrpc.org/specification#error_object
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// Error function is provided to be used as error object.
func (e *RPCError) Error() string {
return strconv.Itoa(e.Code) + ":" + e.Message
}
// HTTPError represents a error that occurred on HTTP level.
//
// An error of type HTTPError is returned when a HTTP error occurred (status code)
// and the body could not be parsed to a valid RPCResponse object that holds a RPCError.
//
// Otherwise a RPCResponse object is returned with a RPCError field that is not nil.
type HTTPError struct {
Code int
err error
}
// Error function is provided to be used as error object.
func (e *HTTPError) Error() string {
return e.err.Error()
}
type rpcClient struct {
endpoint string
httpClient *http.Client
customHeaders map[string]string
}
// RPCClientOpts can be provided to NewClientWithOpts() to change configuration of RPCClient.
//
// HTTPClient: provide a custom http.Client (e.g. to set a proxy, or tls options)
//
// CustomHeaders: provide custom headers, e.g. to set BasicAuth
type RPCClientOpts struct {
HTTPClient *http.Client
CustomHeaders map[string]string
}
// RPCResponses is of type []*RPCResponse.
// This type is used to provide helper functions on the result list
type RPCResponses []*RPCResponse
// AsMap returns the responses as map with response id as key.
func (res RPCResponses) AsMap() map[int]*RPCResponse {
resMap := make(map[int]*RPCResponse, 0)
for _, r := range res {
resMap[r.ID] = r
}
return resMap
}
// GetByID returns the response object of the given id, nil if it does not exist.
func (res RPCResponses) GetByID(id int) *RPCResponse {
for _, r := range res {
if r.ID == id {
return r
}
}
return nil
}
// HasError returns true if one of the response objects has Error field != nil
func (res RPCResponses) HasError() bool {
for _, res := range res {
if res.Error != nil {
return true
}
}
return false
}
// RPCRequests is of type []*RPCRequest.
// This type is used to provide helper functions on the request list
type RPCRequests []*RPCRequest
// NewClient returns a new RPCClient instance with default configuration.
//
// endpoint: JSON-RPC service URL to which JSON-RPC requests are sent.
func NewClient(endpoint string) RPCClient {
return NewClientWithOpts(endpoint, nil)
}
// NewClientWithOpts returns a new RPCClient instance with custom configuration.
//
// endpoint: JSON-RPC service URL to which JSON-RPC requests are sent.
//
// opts: RPCClientOpts provide custom configuration
func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {
rpcClient := &rpcClient{
endpoint: endpoint,
httpClient: &http.Client{},
customHeaders: make(map[string]string),
}
if opts == nil {
return rpcClient
}
if opts.HTTPClient != nil {
rpcClient.httpClient = opts.HTTPClient
}
if opts.CustomHeaders != nil {
for k, v := range opts.CustomHeaders {
rpcClient.customHeaders[k] = v
}
}
return rpcClient
}
func (client *rpcClient) Call(ctx context.Context, method string, params ...interface{}) (*RPCResponse, error) {
request := &RPCRequest{
Method: method,
Params: Params(params...),
JSONRPC: jsonrpcVersion,
}
return client.doCall(ctx, request)
}
func (client *rpcClient) CallForInto(
ctx context.Context,
out interface{},
method string,
params []interface{},
) error {
request := &RPCRequest{
Method: method,
JSONRPC: jsonrpcVersion,
}
if params != nil {
request.Params = params
}
rpcResponse, err := client.doCall(ctx, request)
if err != nil {
return err
}
if rpcResponse.Error != nil {
return rpcResponse.Error
}
return rpcResponse.GetObject(out)
}
func (client *rpcClient) CallRaw(ctx context.Context, request *RPCRequest) (*RPCResponse, error) {
return client.doCall(ctx, request)
}
func (client *rpcClient) CallFor(ctx context.Context, out interface{}, method string, params ...interface{}) error {
rpcResponse, err := client.Call(ctx, method, params...)
if err != nil {
return err
}
if rpcResponse.Error != nil {
return rpcResponse.Error
}
return rpcResponse.GetObject(out)
}
func (client *rpcClient) CallBatch(ctx context.Context, requests RPCRequests) (RPCResponses, error) {
if len(requests) == 0 {
return nil, errors.New("empty request list")
}
for i, req := range requests {
req.ID = i
req.JSONRPC = jsonrpcVersion
}
return client.doBatchCall(ctx, requests)
}
func (client *rpcClient) CallBatchRaw(ctx context.Context, requests RPCRequests) (RPCResponses, error) {
if len(requests) == 0 {
return nil, errors.New("empty request list")
}
return client.doBatchCall(ctx, requests)
}
func (client *rpcClient) newRequest(ctx context.Context, req interface{}) (*http.Request, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
request, err := http.NewRequestWithContext(ctx, "POST", client.endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
// set default headers first, so that even content type and accept can be overwritten
for k, v := range client.customHeaders {
request.Header.Set(k, v)
}
return request, nil
}
func (client *rpcClient) doCall(ctx context.Context, RPCRequest *RPCRequest) (*RPCResponse, error) {
httpRequest, err := client.newRequest(ctx, RPCRequest)
if err != nil {
return nil, fmt.Errorf("rpc call %v() on %v: %v", RPCRequest.Method, httpRequest.URL.String(), err.Error())
}
httpResponse, err := client.httpClient.Do(httpRequest)
if err != nil {
return nil, fmt.Errorf("rpc call %v() on %v: %v", RPCRequest.Method, httpRequest.URL.String(), err.Error())
}
defer httpResponse.Body.Close()
var rpcResponse *RPCResponse
decoder := json.NewDecoder(httpResponse.Body)
decoder.DisallowUnknownFields()
decoder.UseNumber()
err = decoder.Decode(&rpcResponse)
// parsing error
if err != nil {
// if we have some http error, return it
if httpResponse.StatusCode >= 400 {
return nil, &HTTPError{
Code: httpResponse.StatusCode,
err: fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %v", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode, err.Error()),
}
}
return nil, fmt.Errorf("rpc call %v() on %v status code: %v. could not decode body to rpc response: %v", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode, err.Error())
}
// response body empty
if rpcResponse == nil {
// if we have some http error, return it
if httpResponse.StatusCode >= 400 {
return nil, &HTTPError{
Code: httpResponse.StatusCode,
err: fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode),
}
}
return nil, fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", RPCRequest.Method, httpRequest.URL.String(), httpResponse.StatusCode)
}
return rpcResponse, nil
}
func (client *rpcClient) doBatchCall(ctx context.Context, rpcRequest []*RPCRequest) ([]*RPCResponse, error) {
httpRequest, err := client.newRequest(ctx, rpcRequest)
if err != nil {
return nil, fmt.Errorf("rpc batch call on %v: %v", httpRequest.URL.String(), err.Error())
}
httpResponse, err := client.httpClient.Do(httpRequest)
if err != nil {
return nil, fmt.Errorf("rpc batch call on %v: %v", httpRequest.URL.String(), err.Error())
}
defer httpResponse.Body.Close()
var rpcResponse RPCResponses
decoder := json.NewDecoder(httpResponse.Body)
decoder.DisallowUnknownFields()
decoder.UseNumber()
err = decoder.Decode(&rpcResponse)
// parsing error
if err != nil {
// if we have some http error, return it
if httpResponse.StatusCode >= 400 {
return nil, &HTTPError{
Code: httpResponse.StatusCode,
err: fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %v", httpRequest.URL.String(), httpResponse.StatusCode, err.Error()),
}
}
return nil, fmt.Errorf("rpc batch call on %v status code: %v. could not decode body to rpc response: %v", httpRequest.URL.String(), httpResponse.StatusCode, err.Error())
}
// response body empty
if rpcResponse == nil || len(rpcResponse) == 0 {
// if we have some http error, return it
if httpResponse.StatusCode >= 400 {
return nil, &HTTPError{
Code: httpResponse.StatusCode,
err: fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode),
}
}
return nil, fmt.Errorf("rpc batch call on %v status code: %v. rpc response missing", httpRequest.URL.String(), httpResponse.StatusCode)
}
return rpcResponse, nil
}
// Params is a helper function that uses the same parameter syntax as Call().
// But you should consider to always use NewRequest() instead.
//
// e.g. to manually create an RPCRequest object:
// request := &RPCRequest{
// Method: "myMethod",
// Params: Params("Alex", 35, true),
// }
//
// same with new request:
// request := NewRequest("myMethod", "Alex", 35, true)
//
// If you know what you are doing you can omit the Params() call but potentially create incorrect rpc requests:
//request := &RPCRequest{
// Method: "myMethod",
// Params: 2, <-- invalid since a single primitive value must be wrapped in an array --> no magic without Params()
// }
//
// correct:
// request := &RPCRequest{
// Method: "myMethod",
// Params: []int{2}, <-- invalid since a single primitive value must be wrapped in an array
// }
func Params(params ...interface{}) interface{} {
var finalParams interface{}
// if params was nil skip this and p stays nil
if params != nil {
switch len(params) {
case 0: // no parameters were provided, do nothing so finalParam is nil and will be omitted
case 1: // one param was provided, use it directly as is, or wrap primitive types in array
if params[0] != nil {
var typeOf reflect.Type
// traverse until nil or not a pointer type
for typeOf = reflect.TypeOf(params[0]); typeOf != nil && typeOf.Kind() == reflect.Ptr; typeOf = typeOf.Elem() {
}
if typeOf != nil {
// now check if we can directly marshal the type or if it must be wrapped in an array
switch typeOf.Kind() {
// for these types we just do nothing, since value of p is already unwrapped from the array params
case reflect.Struct:
finalParams = params[0]
case reflect.Array:
finalParams = params[0]
case reflect.Slice:
finalParams = params[0]
case reflect.Interface:
finalParams = params[0]
case reflect.Map:
finalParams = params[0]
default: // everything else must stay in an array (int, string, etc)
finalParams = params
}
}
} else {
finalParams = params
}
default: // if more than one parameter was provided it should be treated as an array
finalParams = params
}
}
return finalParams
}
// GetInt converts the rpc response to an int64 and returns it.
//
// If result was not an integer an error is returned.
func (RPCResponse *RPCResponse) GetInt() (int64, error) {
val, ok := RPCResponse.Result.(json.Number)
if !ok {
return 0, fmt.Errorf("could not parse int64 from %s", RPCResponse.Result)
}
i, err := val.Int64()
if err != nil {
return 0, err
}
return i, nil
}
// GetFloat converts the rpc response to float64 and returns it.
//
// If result was not an float64 an error is returned.
func (RPCResponse *RPCResponse) GetFloat() (float64, error) {
val, ok := RPCResponse.Result.(json.Number)
if !ok {
return 0, fmt.Errorf("could not parse float64 from %s", RPCResponse.Result)
}
f, err := val.Float64()
if err != nil {
return 0, err
}
return f, nil
}
// GetBool converts the rpc response to a bool and returns it.
//
// If result was not a bool an error is returned.
func (RPCResponse *RPCResponse) GetBool() (bool, error) {
val, ok := RPCResponse.Result.(bool)
if !ok {
return false, fmt.Errorf("could not parse bool from %s", RPCResponse.Result)
}
return val, nil
}
// GetString converts the rpc response to a string and returns it.
//
// If result was not a string an error is returned.
func (RPCResponse *RPCResponse) GetString() (string, error) {
val, ok := RPCResponse.Result.(string)
if !ok {
return "", fmt.Errorf("could not parse string from %s", RPCResponse.Result)
}
return val, nil
}
// GetObject converts the rpc response to an arbitrary type.
//
// The function works as you would expect it from json.Unmarshal()
func (RPCResponse *RPCResponse) GetObject(toType interface{}) error {
js, err := json.Marshal(RPCResponse.Result)
if err != nil {
return err
}
err = json.Unmarshal(js, toType)
if err != nil {
return err
}
return nil
}

1136
rpc/jsonrpc/jsonrpc_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,6 @@ import (
// has information about in its ledger. This value may increase
// over time if the node is configured to purge older ledger data.
func (cl *Client) MinimumLedgerSlot(ctx context.Context) (out uint64, err error) {
err = cl.rpcClient.CallFor(&out, "minimumLedgerSlot")
err = cl.rpcClient.CallForInto(ctx, &out, "minimumLedgerSlot", nil)
return
}

View File

@ -36,6 +36,6 @@ func (cl *Client) RequestAirdrop(
M{"commitment": commitment},
)
}
err = cl.rpcClient.CallFor(&signature, "requestAirdrop", params)
err = cl.rpcClient.CallForInto(ctx, &signature, "requestAirdrop", params)
return
}

View File

@ -3,8 +3,27 @@ package rpc
// See more: https://docs.solana.com/cluster/rpc-endpoints
const (
EndpointDevNet = "https://api.devnet.solana.com"
EndpointTestNet = "https://api.testnet.solana.com"
EndpointMainNetBeta = "https://api.mainnet-beta.solana.com"
EndpointMainNetBetaSerum = "https://solana-api.projectserum.com"
protocolHTTPS = "https://"
protocolWSS = "wss://"
)
const (
hostDevNet = "api.devnet.solana.com"
hostTestNet = "api.testnet.solana.com"
hostMainNetBeta = "api.mainnet-beta.solana.com"
hostMainNetBetaSerum = "solana-api.projectserum.com"
)
const (
EndpointRPCDevNet = protocolHTTPS + "api.devnet.solana.com"
EndpointRPCTestNet = protocolHTTPS + "api.testnet.solana.com"
EndpointRPCMainNetBeta = protocolHTTPS + "api.mainnet-beta.solana.com"
EndpointRPCMainNetBetaSerum = protocolHTTPS + "solana-api.projectserum.com"
)
const (
EndpointWSDevNet = protocolWSS + "api.devnet.solana.com"
EndpointWSTestNet = protocolWSS + "api.testnet.solana.com"
EndpointWSMainNetBeta = protocolWSS + "api.mainnet-beta.solana.com"
EndpointWSMainNetBetaSerum = protocolWSS + "solana-api.projectserum.com"
)

View File

@ -92,6 +92,6 @@ func (cl *Client) SendTransactionWithOpts(
obj,
}
err = cl.rpcClient.CallFor(&signature, "sendTransaction", params)
err = cl.rpcClient.CallForInto(ctx, &signature, "sendTransaction", params)
return
}

View File

@ -120,6 +120,6 @@ func (cl *Client) SimulateTransactionWithOpts(
obj,
}
err = cl.rpcClient.CallFor(&out, "simulateTransaction", params)
err = cl.rpcClient.CallForInto(ctx, &out, "simulateTransaction", params)
return
}

View File

@ -40,9 +40,10 @@ type Client struct {
reconnectOnErr bool
}
func Dial(ctx context.Context, rpcURL string) (c *Client, err error) {
// Connect creates a new websocket client connecting to the provided endpoint.
func Connect(ctx context.Context, rpcEndpoint string) (c *Client, err error) {
c = &Client{
rpcURL: rpcURL,
rpcURL: rpcEndpoint,
subscriptionByRequestID: map[uint64]*Subscription{},
subscriptionByWSSubID: map[uint64]*Subscription{},
}
@ -53,7 +54,7 @@ func Dial(ctx context.Context, rpcURL string) (c *Client, err error) {
EnableCompression: true,
}
c.conn, _, err = dialer.DialContext(ctx, rpcURL, nil)
c.conn, _, err = dialer.DialContext(ctx, rpcEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("new ws client: dial: %w", err)
}

View File

@ -31,7 +31,7 @@ func Test_AccountSubscribe(t *testing.T) {
zlog, _ = zap.NewDevelopment()
c, err := Dial(context.Background(), "ws://api.mainnet-beta.solana.com:80")
c, err := Connect(context.Background(), "ws://api.mainnet-beta.solana.com:80")
defer c.Close()
require.NoError(t, err)
@ -56,7 +56,7 @@ func Test_ProgramSubscribe(t *testing.T) {
zlog, _ = zap.NewDevelopment()
fmt.Println("Dialing")
c, err := Dial(context.Background(), "wss://solana-api.projectserum.com")
c, err := Connect(context.Background(), "wss://solana-api.projectserum.com")
fmt.Println("Hello?")
defer c.Close()
require.NoError(t, err)
@ -80,7 +80,7 @@ func Test_SlotSubscribe(t *testing.T) {
zlog, _ = zap.NewDevelopment()
c, err := Dial(context.Background(), "ws://api.mainnet-beta.solana.com:80")
c, err := Connect(context.Background(), "ws://api.mainnet-beta.solana.com:80")
defer c.Close()
require.NoError(t, err)