diff --git a/client.go b/client.go new file mode 100644 index 0000000..2a7a74b --- /dev/null +++ b/client.go @@ -0,0 +1,121 @@ +package cnc + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + c *resty.Client +} + +func NewClient(baseURL string, options ...Option) (*Client, error) { + c := &Client{ + c: resty.New(), + } + + c.c.SetBaseURL(baseURL) + + for _, option := range options { + if err := option(c); err != nil { + return nil, err + } + } + + return c, nil +} + +func (c *Client) Header(ctx context.Context, height uint64) /* Header */ error { + _ = headerPath() + return errors.New("method Header not implemented") +} + +func (c *Client) Balance(ctx context.Context) error { + _ = balanceEndpoint + return errors.New("method Balance not implemented") +} + +func (c *Client) SubmitTx(ctx context.Context, tx []byte) /* TxResponse */ error { + _ = submitTxEndpoint + return errors.New("method SubmitTx not implemented") +} + +func (c *Client) SubmitPFD(ctx context.Context, namespaceID [8]byte, data []byte, gasLimit uint64) (*TxResponse, error) { + req := SubmitPFDRequest{ + NamespaceID: hex.EncodeToString(namespaceID[:]), + Data: hex.EncodeToString(data), + GasLimit: gasLimit, + } + var res TxResponse + var rpcErr string + _, err := c.c.R(). + SetContext(ctx). + SetBody(req). + SetResult(&res). + SetError(&rpcErr). + Post(submitPFDEndpoint) + if err != nil { + return nil, err + } + if rpcErr != "" { + return nil, errors.New(rpcErr) + } + return &res, nil +} + +func (c *Client) NamespacedShares(ctx context.Context, namespaceID [8]byte, height uint64) ([][]byte, error) { + var res struct { + Shares [][]byte `json:"shares"` + Height uint64 `json:"height"` + } + + err := c.callNamespacedEndpoint(ctx, namespaceID, height, namespacedSharesEndpoint, &res) + if err != nil { + return nil, err + } + + return res.Shares, nil +} + +func (c *Client) NamespacedData(ctx context.Context, namespaceID [8]byte, height uint64) ([][]byte, error) { + var res struct { + Data [][]byte `json:"data"` + Height uint64 `json:"height"` + } + + err := c.callNamespacedEndpoint(ctx, namespaceID, height, namespacedDataEndpoint, &res) + if err != nil { + return nil, err + } + + return res.Data, nil +} + +// callNamespacedEndpoint fetches result of /namespaced_{type} family of endpoints into result (this should be pointer!) +func (c *Client) callNamespacedEndpoint(ctx context.Context, namespaceID [8]byte, height uint64, endpoint string, result interface{}) error { + var rpcErr string + _, err := c.c.R(). + SetContext(ctx). + SetResult(result). + SetError(&rpcErr). + Get(namespacedPath(endpoint, namespaceID, height)) + if err != nil { + return err + } + if rpcErr != "" { + return errors.New(rpcErr) + } + return nil +} + +func headerPath() string { + return fmt.Sprintf("%s/%s", headerEndpoint, heightKey) +} + +func namespacedPath(endpoint string, namespaceID [8]byte, height uint64) string { + return fmt.Sprintf("%s/%s/height/%d", endpoint, hex.EncodeToString(namespaceID[:]), height) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..c9e97a8 --- /dev/null +++ b/client_test.go @@ -0,0 +1,55 @@ +package cnc + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestNewClient(t *testing.T) { + cases := []struct { + name string + options []Option + expectedError error + }{ + {"without options", nil, nil}, + {"with timeout", []Option{WithTimeout(1 * time.Second)}, nil}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + client, err := NewClient("", c.options...) + assert.ErrorIs(t, err, c.expectedError) + if c.expectedError != nil { + assert.Nil(t, client) + } else { + assert.NotNil(t, client) + } + }) + } +} + +func TestNamespacedShares(t *testing.T) { + t.Skip() + client, err := NewClient("http://localhost:26658") + assert.NoError(t, err) + assert.NotNil(t, client) + + shares, err := client.NamespacedShares(context.TODO(), [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 8) + assert.NoError(t, err) + assert.NotNil(t, shares) + assert.Len(t, shares, 4) +} + +func TestSubmitPDF(t *testing.T) { + t.Skip() + client, err := NewClient("http://localhost:26658", WithTimeout(30*time.Second)) + assert.NoError(t, err) + assert.NotNil(t, client) + + txRes, err := client.SubmitPFD(context.TODO(), [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, []byte("random data"), 100000) + assert.NoError(t, err) + assert.NotNil(t, txRes) +} diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..68e2282 --- /dev/null +++ b/consts.go @@ -0,0 +1,12 @@ +package cnc + +const ( + headerEndpoint = "/header" + balanceEndpoint = "/balance" + submitTxEndpoint = "/submit_tx" + submitPFDEndpoint = "/submit_pfd" + namespacedSharesEndpoint = "/namespaced_shares" + namespacedDataEndpoint = "/namespaced_data" + + heightKey = "height" +) diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..5c124e8 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package cnrc implements a Celestia Node REST Client +package cnc diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eb63b5e --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/celestiaorg/go-cnc + +go 1.17 + +require ( + github.com/go-resty/resty/v2 v2.7.0 + github.com/gogo/protobuf v1.3.2 + github.com/stretchr/testify v1.7.1 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.0.0-20211029224645-99673261e6eb // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..19c73be --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= +github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= +golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/options.go b/options.go new file mode 100644 index 0000000..8acad66 --- /dev/null +++ b/options.go @@ -0,0 +1,12 @@ +package cnc + +import "time" + +type Option func(*Client) error + +func WithTimeout(timeout time.Duration) Option { + return func(c *Client) error { + c.c.SetTimeout(timeout) + return nil + } +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..437279e --- /dev/null +++ b/types.go @@ -0,0 +1,78 @@ +package cnc + +import "github.com/gogo/protobuf/types" + +// SubmitPFDRequest represents a request to submit a PayForData transaction. +type SubmitPFDRequest struct { + NamespaceID string `json:"namespace_id"` + Data string `json:"data"` + GasLimit uint64 `json:"gas_limit"` +} + +// Types below are copied from celestia-node (or cosmos-sdk dependency of celestia node, to be precise) +// They are needed for proper deserialization. +// It's probably far from the best approach to those types, but it's simple and works. +// Some alternatives: +// 1. Generate types from protobuf definitions (and automate updating of protobuf files) +// 2. Extract common dependency that defines all types used in RPC. + +// TxResponse defines a structure containing relevant tx data and metadata. The +// tags are stringified and the log is JSON decoded. +type TxResponse struct { + // The block height + Height int64 `protobuf:"varint,1,opt,name=height,proto3" json:"height,omitempty"` + // The transaction hash. + TxHash string `protobuf:"bytes,2,opt,name=txhash,proto3" json:"txhash,omitempty"` + // Namespace for the Code + Codespace string `protobuf:"bytes,3,opt,name=codespace,proto3" json:"codespace,omitempty"` + // Response code. + Code uint32 `protobuf:"varint,4,opt,name=code,proto3" json:"code,omitempty"` + // Result bytes, if any. + Data string `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` + // The output of the application's logger (raw string). May be + // non-deterministic. + RawLog string `protobuf:"bytes,6,opt,name=raw_log,json=rawLog,proto3" json:"raw_log,omitempty"` + // The output of the application's logger (typed). May be non-deterministic. + Logs ABCIMessageLogs `protobuf:"bytes,7,rep,name=logs,proto3,castrepeated=ABCIMessageLogs" json:"logs"` + // Additional information. May be non-deterministic. + Info string `protobuf:"bytes,8,opt,name=info,proto3" json:"info,omitempty"` + // Amount of gas requested for transaction. + GasWanted int64 `protobuf:"varint,9,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` + // Amount of gas consumed by transaction. + GasUsed int64 `protobuf:"varint,10,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + // The request transaction bytes. + Tx *types.Any `protobuf:"bytes,11,opt,name=tx,proto3" json:"tx,omitempty"` + // Time of the previous block. For heights > 1, it's the weighted median of + // the timestamps of the valid votes in the block.LastCommit. For height == 1, + // it's genesis time. + Timestamp string `protobuf:"bytes,12,opt,name=timestamp,proto3" json:"timestamp,omitempty"` +} + +// ABCIMessageLogs represents a slice of ABCIMessageLog. +type ABCIMessageLogs []ABCIMessageLog + +// ABCIMessageLog defines a structure containing an indexed tx ABCI message log. +type ABCIMessageLog struct { + MsgIndex uint32 `protobuf:"varint,1,opt,name=msg_index,json=msgIndex,proto3" json:"msg_index,omitempty"` + Log string `protobuf:"bytes,2,opt,name=log,proto3" json:"log,omitempty"` + // Events contains a slice of Event objects that were emitted during some + // execution. + Events StringEvents `protobuf:"bytes,3,rep,name=events,proto3,castrepeated=StringEvents" json:"events"` +} + +// StringAttributes defines a slice of StringEvents objects. +type StringEvents []StringEvent + +// StringEvent defines en Event object wrapper where all the attributes +// contain key/value pairs that are strings instead of raw bytes. +type StringEvent struct { + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Attributes []Attribute `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes"` +} + +// Attribute defines an attribute wrapper where the key and value are +// strings instead of raw bytes. +type Attribute struct { + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` +}