From 98429c58b00b357cf908f20faf9b3f6d42e1e0e1 Mon Sep 17 00:00:00 2001 From: Nikolay Eskov Date: Thu, 10 Jun 2021 02:55:48 +0300 Subject: [PATCH] Created new node HTTP API route: GET /alias/by-alias/{alias} --- cmd/custom/node.go | 6 +++- cmd/node/node.go | 7 ++++- pkg/api/app.go | 24 ++++++++++++---- pkg/api/app_alias.go | 18 ++++++++++++ pkg/api/app_blocks_test.go | 4 +-- pkg/api/app_peers_test.go | 4 +-- pkg/api/app_test.go | 2 +- pkg/api/errors/validation.go | 21 ++++++++++++++ pkg/api/helpers.go | 25 ++++++++++++++++- pkg/api/node_api.go | 54 +++++++++++++++++++++++------------- pkg/api/node_api_test.go | 35 +++++++++++++++++++++++ pkg/api/routes.go | 4 +++ pkg/proto/address.go | 34 ++++++++++++++++++++--- pkg/proto/address_test.go | 26 +++++++++++++++++ 14 files changed, 227 insertions(+), 37 deletions(-) create mode 100644 pkg/api/app_alias.go diff --git a/cmd/custom/node.go b/cmd/custom/node.go index a6c9ae0827..318a1009f5 100644 --- a/cmd/custom/node.go +++ b/cmd/custom/node.go @@ -272,8 +272,12 @@ func main() { } } + apiConfig := api.AppConfig{ + BlockchainType: "", + BuildVersion: buildVersion, + } // TODO hardcore - app, err := api.NewApp("integration-test-rest-api", scheduler, nodeServices, buildVersion) + app, err := api.NewApp("integration-test-rest-api", scheduler, nodeServices, apiConfig) if err != nil { zap.S().Error(err) cancel() diff --git a/cmd/node/node.go b/cmd/node/node.go index e11504e825..5f62afb1cf 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -383,7 +383,12 @@ func main() { } } - app, err := api.NewApp(*apiKey, minerScheduler, svs, buildVersion) + apiConfig := api.AppConfig{ + BlockchainType: *blockchainType, + BuildVersion: buildVersion, + } + + app, err := api.NewApp(*apiKey, minerScheduler, svs, apiConfig) if err != nil { zap.S().Error(err) cancel() diff --git a/pkg/api/app.go b/pkg/api/app.go index 0682874c8a..8f253327be 100644 --- a/pkg/api/app.go +++ b/pkg/api/app.go @@ -35,10 +35,24 @@ type App struct { peers peer_manager.PeerManager sync types.StateSync services services.Services - buildVersion string + config AppConfig } -func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services, buildVersion string) (*App, error) { +type AppConfig struct { + BlockchainType string + BuildVersion string +} + +func (ac *AppConfig) Validate() error { + // TODO(nickeskov): implement me + return nil +} + +func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services, config AppConfig) (*App, error) { + if err := config.Validate(); err != nil { + return nil, err + } + digest, err := crypto.SecureHash([]byte(apiKey)) if err != nil { return nil, err @@ -52,12 +66,12 @@ func NewApp(apiKey string, scheduler SchedulerEmits, services services.Services, utx: services.UtxPool, peers: services.Peers, services: services, - buildVersion: buildVersion, + config: config, }, nil } -func (a *App) BuildVersion() string { - return a.buildVersion +func (a *App) Config() *AppConfig { + return &a.config } func (a *App) TransactionsBroadcast(ctx context.Context, b []byte) error { diff --git a/pkg/api/app_alias.go b/pkg/api/app_alias.go new file mode 100644 index 0000000000..aa9424aeff --- /dev/null +++ b/pkg/api/app_alias.go @@ -0,0 +1,18 @@ +package api + +import ( + "github.com/pkg/errors" + "github.com/wavesplatform/gowaves/pkg/proto" + "github.com/wavesplatform/gowaves/pkg/state" +) + +func (a *App) AddrByAlias(alias proto.Alias) (proto.Address, error) { + addr, err := a.state.AddrByAlias(alias) + if err != nil { + if state.IsNotFound(err) { + return proto.Address{}, err + } + return proto.Address{}, errors.Wrapf(err, "failed to find addr by alias %q", alias.String()) + } + return addr, nil +} diff --git a/pkg/api/app_blocks_test.go b/pkg/api/app_blocks_test.go index f5e9eb575e..97cc6e3e8e 100644 --- a/pkg/api/app_blocks_test.go +++ b/pkg/api/app_blocks_test.go @@ -24,7 +24,7 @@ func TestApp_BlocksFirst(t *testing.T) { s := mock.NewMockState(ctrl) s.EXPECT().BlockByHeight(proto.Height(1)).Return(g, nil) - app, err := NewApp("api-key", nil, services.Services{State: s}, "") + app, err := NewApp("api-key", nil, services.Services{State: s}, AppConfig{}) require.NoError(t, err) first, err := app.BlocksFirst() require.NoError(t, err) @@ -40,7 +40,7 @@ func TestApp_BlocksLast(t *testing.T) { s.EXPECT().Height().Return(proto.Height(1), nil) s.EXPECT().BlockByHeight(proto.Height(1)).Return(g, nil) - app, err := NewApp("api-key", nil, services.Services{State: s}, "") + app, err := NewApp("api-key", nil, services.Services{State: s}, AppConfig{}) require.NoError(t, err) first, err := app.BlocksLast() require.NoError(t, err) diff --git a/pkg/api/app_peers_test.go b/pkg/api/app_peers_test.go index cfb0f2c33e..00ea8e417c 100644 --- a/pkg/api/app_peers_test.go +++ b/pkg/api/app_peers_test.go @@ -22,7 +22,7 @@ func TestApp_PeersKnown(t *testing.T) { addr := proto.NewTCPAddr(net.ParseIP("127.0.0.1"), 6868).ToIpPort() peerManager.EXPECT().KnownPeers().Return([]storage.KnownPeer{storage.KnownPeer(addr)}) - app, err := NewApp("key", nil, services.Services{Peers: peerManager}, "") + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, AppConfig{}) require.NoError(t, err) rs2, err := app.PeersKnown() @@ -56,7 +56,7 @@ func TestApp_PeersSuspended(t *testing.T) { peerManager.EXPECT().Suspended().Return(testData) - app, err := NewApp("key", nil, services.Services{Peers: peerManager}, "") + app, err := NewApp("key", nil, services.Services{Peers: peerManager}, AppConfig{}) require.NoError(t, err) suspended := app.PeersSuspended() diff --git a/pkg/api/app_test.go b/pkg/api/app_test.go index cde801cf2e..32198f5fa7 100644 --- a/pkg/api/app_test.go +++ b/pkg/api/app_test.go @@ -8,7 +8,7 @@ import ( ) func TestAppAuth(t *testing.T) { - app, _ := NewApp("apiKey", nil, services.Services{}, "") + app, _ := NewApp("apiKey", nil, services.Services{}, AppConfig{}) require.Error(t, app.checkAuth("bla")) require.NoError(t, app.checkAuth("apiKey")) } diff --git a/pkg/api/errors/validation.go b/pkg/api/errors/validation.go index dd3a59b6f4..ba6e993eff 100644 --- a/pkg/api/errors/validation.go +++ b/pkg/api/errors/validation.go @@ -2,6 +2,7 @@ package errors import ( "encoding/json" + "fmt" "github.com/pkg/errors" "net/http" ) @@ -152,3 +153,23 @@ var ( }, } ) + +func NewCustomValidationError(msg string) *CustomValidationError { + return &CustomValidationError{ + genericError: genericError{ + ID: CustomValidationErrorErrorID, + HttpCode: http.StatusBadRequest, + Message: msg, + }, + } +} + +func NewAliasDoesNotExistError(aliasFull string) *AliasDoesNotExistError { + return &AliasDoesNotExistError{ + genericError: genericError{ + ID: AliasDoesNotExistErrorID, + HttpCode: http.StatusNotFound, + Message: fmt.Sprintf("alias '%s' doesn't exist", aliasFull), + }, + } +} diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index 1813616414..f76576fabc 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -1,6 +1,11 @@ package api -import "time" +import ( + "encoding/json" + "github.com/pkg/errors" + "io" + "time" +) func unixMillis(t time.Time) int64 { return t.UnixNano() / 1_000_000 @@ -11,3 +16,21 @@ func fromUnixMillis(timestampMillis int64) time.Time { nsec := (timestampMillis % 1_000) * 1_000_000 return time.Unix(sec, nsec) } + +// tryParseJson receives reader and out params. out MUST be a pointer +func tryParseJson(r io.Reader, out interface{}) error { + // TODO(nickeskov): check empty reader + err := json.NewDecoder(r).Decode(out) + if err != nil { + return errors.Wrapf(err, "Failed to unmarshal %T as JSON into %T", r, out) + } + return nil +} + +func trySendJson(w io.Writer, v interface{}) error { + err := json.NewEncoder(w).Encode(v) + if err != nil { + return errors.Wrapf(err, "Failed to marshal %T to JSON and write it to %T", v, w) + } + return nil +} diff --git a/pkg/api/node_api.go b/pkg/api/node_api.go index 059ba8d2b4..e5fd006041 100644 --- a/pkg/api/node_api.go +++ b/pkg/api/node_api.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "io/ioutil" "net/http" "os" @@ -472,7 +471,7 @@ func (a *NodeApi) BuildVersion(w http.ResponseWriter, _ *http.Request) error { Version string `json:"version"` } - buildVersion := a.app.BuildVersion() + buildVersion := a.app.Config().BuildVersion out := ver{Version: fmt.Sprintf("GoWaves %s", buildVersion)} if err := trySendJson(w, out); err != nil { @@ -481,6 +480,39 @@ func (a *NodeApi) BuildVersion(w http.ResponseWriter, _ *http.Request) error { return nil } +func (a *NodeApi) AddrByAlias(w http.ResponseWriter, r *http.Request) error { + type addrResponse struct { + Address string `json:"address"` + } + + // nickeskov: alias as plain text without an 'alias' prefix and chain ID (scheme) + aliasShort := chi.URLParam(r, "alias") + + chainID := proto.SchemeFromString(a.app.Config().BlockchainType) + + alias := proto.NewAlias(chainID, aliasShort) + if _, err := alias.Valid(); err != nil { + // TODO(nickeskov): check that error msg looks like in scala + msg := err.Error() + return apiErrs.NewCustomValidationError(msg) + } + + addr, err := a.app.AddrByAlias(*alias) + if err != nil { + origErr := errors.Cause(err) + if state.IsNotFound(origErr) { + return apiErrs.NewAliasDoesNotExistError(alias.String()) + } + return errors.Wrapf(err, "failed to find addr by short alias %q", aliasShort) + } + + resp := addrResponse{Address: addr.String()} + if err := trySendJson(w, resp); err != nil { + return errors.Wrap(err, "AddrByAlias") + } + return nil +} + func (a *NodeApi) nodeProcesses(w http.ResponseWriter, _ *http.Request) error { rs := a.app.NodeProcesses() if err := trySendJson(w, rs); err != nil { @@ -548,21 +580,3 @@ func (a *NodeApi) walletSeed(w http.ResponseWriter, _ *http.Request) error { } return nil } - -// tryParseJson receives reader and out params. out MUST be a pointer -func tryParseJson(r io.Reader, out interface{}) error { - // TODO(nickeskov): check empty reader - err := json.NewDecoder(r).Decode(out) - if err != nil { - return errors.Wrapf(err, "Failed to unmarshal %T as JSON into %T", r, out) - } - return nil -} - -func trySendJson(w io.Writer, v interface{}) error { - err := json.NewEncoder(w).Encode(v) - if err != nil { - return errors.Wrapf(err, "Failed to marshal %T to JSON and write it to %T", v, w) - } - return nil -} diff --git a/pkg/api/node_api_test.go b/pkg/api/node_api_test.go index 36e0d47b5d..b01a5f3f3a 100644 --- a/pkg/api/node_api_test.go +++ b/pkg/api/node_api_test.go @@ -1,6 +1,12 @@ package api import ( + "context" + "github.com/go-chi/chi" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/wavesplatform/gowaves/pkg/mock" + "net/http" "net/http/httptest" "strings" "testing" @@ -79,3 +85,32 @@ func TestNodeApi_FindFirstInvalidRuneInBase58String(t *testing.T) { actual := findFirstInvalidRuneInBase58String("42354") assert.Nil(t, actual) } + +func TestNodeApi_AddrByAlias(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testAlias := "some_alias" + + req := httptest.NewRequest(http.MethodGet, "/", nil) + resp := httptest.NewRecorder() + + stateMock := mock.NewMockState(ctrl) + stateMock.EXPECT(). + AddrByAlias(*proto.NewAlias(proto.TestNetScheme, testAlias)). + Return(proto.Address{}, nil) + + api := NodeApi{ + app: &App{ + state: stateMock, + config: AppConfig{BlockchainType: "testnet"}, + }, + } + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("alias", testAlias) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + err := api.AddrByAlias(resp, req) + require.NoError(t, err) +} diff --git a/pkg/api/routes.go b/pkg/api/routes.go index b0589a3250..03391ff0f8 100644 --- a/pkg/api/routes.go +++ b/pkg/api/routes.go @@ -105,6 +105,10 @@ func (a *NodeApi) routes(opts *RunOptions) (chi.Router, error) { r.Get("/", wrapper(a.Addresses)) }) + r.Route("/alias", func(r chi.Router) { + r.Get("/by-alias/{alias}", wrapper(a.AddrByAlias)) + }) + r.Route("/transactions", func(r chi.Router) { r.Get("/unconfirmed/size", wrapper(a.unconfirmedSize)) diff --git a/pkg/proto/address.go b/pkg/proto/address.go index 69a31fdb8f..2258d31fe7 100644 --- a/pkg/proto/address.go +++ b/pkg/proto/address.go @@ -31,10 +31,10 @@ const ( AliasAlphabet = "-.0123456789@_abcdefghijklmnopqrstuvwxyz" AliasPrefix = "alias" - MainNetScheme byte = 'W' - TestNetScheme byte = 'T' - StageNetScheme byte = 'S' - CustomNetScheme byte = 'E' + MainNetScheme Scheme = 'W' + TestNetScheme Scheme = 'T' + StageNetScheme Scheme = 'S' + CustomNetScheme Scheme = 'E' ) // Address is the transformed Public Key with additional bytes of the version, a blockchain scheme and a checksum. @@ -531,3 +531,29 @@ func (r *Recipient) String() string { } return r.Address.String() } + +// SchemeFromString returns Scheme from string representation (short or full). +func SchemeFromString(scheme string) Scheme { + switch len(scheme) { + case 0: + return CustomNetScheme + case 1: + switch b := scheme[0]; b { + case MainNetScheme, StageNetScheme, TestNetScheme: + return b + default: + return CustomNetScheme + } + default: + switch scheme { + case "mainnet": + return MainNetScheme + case "stagenet": + return StageNetScheme + case "testnet": + return TestNetScheme + default: + return CustomNetScheme + } + } +} diff --git a/pkg/proto/address_test.go b/pkg/proto/address_test.go index c9d8ee2761..70053f93c3 100644 --- a/pkg/proto/address_test.go +++ b/pkg/proto/address_test.go @@ -169,3 +169,29 @@ func TestRecipient_WriteTo(t *testing.T) { require.Equal(t, bin, buf.Bytes()) } + +func TestSchemeFromString(t *testing.T) { + tests := []struct { + schemeStr string + expectedScheme Scheme + }{ + {"", CustomNetScheme}, + + {"W", MainNetScheme}, + {"S", StageNetScheme}, + {"T", TestNetScheme}, + + {"E", CustomNetScheme}, + {"@", CustomNetScheme}, + + {"mainnet", MainNetScheme}, + {"stagenet", StageNetScheme}, + {"testnet", TestNetScheme}, + {"any-string", CustomNetScheme}, + } + + for _, testCase := range tests { + actualScheme := SchemeFromString(testCase.schemeStr) + require.Equal(t, testCase.expectedScheme, actualScheme) + } +}