From 4d9a49e7f7fb1975cd0fe557e0f0624c6f6e615c Mon Sep 17 00:00:00 2001 From: Yordan Miladinov Date: Wed, 28 Jul 2021 02:22:20 +0300 Subject: [PATCH] gctrpc: endpoints now optionally return timestamps in nanoseconds (configurable) (#718) * config: add remoteControl/gRPC/timeInNanoSeconds * grpc: consult with remoteControl/gRPC/timeInNanoSeconds whether timestamps should be in seconds or nanos * engine: test if RPCServer.unixTimestamp() respects config/remoteControl/gRPC/timeInNanoSeconds * engine: implement TestRPCServer_GetTicker_LastUpdatedNanos that makes sure TickerResponse.LastUpdated is returned in nanoseconds if configured * config_example.json: add remoteControl/gRPC/timeInNanoSeconds * engine: (1) test RPCServer.unixTimestamp() in parallel, (2) increase time tolerance of TestRPCServer_GetTicker_LastUpdatedNanos() * engine: TestRPCServer_GetTicker_LastUpdatedNanos() now fetches a mock-up ticker to check timestamps --- config/config_types.go | 1 + config_example.json | 5 ++- engine/rpcserver.go | 43 ++++++++++-------- engine/rpcserver_test.go | 94 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 19 deletions(-) diff --git a/config/config_types.go b/config/config_types.go index 01630896..eb79b010 100644 --- a/config/config_types.go +++ b/config/config_types.go @@ -179,6 +179,7 @@ type GRPCConfig struct { ListenAddress string `json:"listenAddress"` GRPCProxyEnabled bool `json:"grpcProxyEnabled"` GRPCProxyListenAddress string `json:"grpcProxyListenAddress"` + TimeInNanoSeconds bool `json:"timeInNanoSeconds"` } // DepcrecatedRPCConfig stores the deprecatedRPCConfig settings diff --git a/config_example.json b/config_example.json index 85cb29f8..68d8a324 100644 --- a/config_example.json +++ b/config_example.json @@ -183,7 +183,8 @@ "enabled": true, "listenAddress": "localhost:9052", "grpcProxyEnabled": false, - "grpcProxyListenAddress": "localhost:9053" + "grpcProxyListenAddress": "localhost:9053", + "timeInNanoSeconds": false }, "deprecatedRPC": { "enabled": true, @@ -2451,4 +2452,4 @@ "supportedExchanges": "Kraken,Bitstamp" } ] -} \ No newline at end of file +} diff --git a/engine/rpcserver.go b/engine/rpcserver.go index c6aa84c2..06839969 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -363,7 +363,7 @@ func (s *RPCServer) GetTicker(_ context.Context, r *gctrpc.GetTickerRequest) (*g resp := &gctrpc.TickerResponse{ Pair: r.Pair, - LastUpdated: t.LastUpdated.Unix(), + LastUpdated: s.unixTimestamp(t.LastUpdated), Last: t.Last, High: t.High, Low: t.Low, @@ -394,7 +394,7 @@ func (s *RPCServer) GetTickers(_ context.Context, _ *gctrpc.GetTickersRequest) ( Base: val.Pair.Base.String(), Quote: val.Pair.Quote.String(), }, - LastUpdated: val.LastUpdated.Unix(), + LastUpdated: s.unixTimestamp(val.LastUpdated), Last: val.Last, High: val.High, Low: val.Low, @@ -456,7 +456,7 @@ func (s *RPCServer) GetOrderbook(_ context.Context, r *gctrpc.GetOrderbookReques Pair: r.Pair, Bids: bids, Asks: asks, - LastUpdated: ob.LastUpdated.Unix(), + LastUpdated: s.unixTimestamp(ob.LastUpdated), AssetType: r.AssetType, } @@ -500,7 +500,7 @@ func (s *RPCServer) GetOrderbooks(_ context.Context, _ *gctrpc.GetOrderbooksRequ Quote: currencies[z].Quote.String(), }, AssetType: assets[y].String(), - LastUpdated: resp.LastUpdated.Unix(), + LastUpdated: s.unixTimestamp(resp.LastUpdated), } for i := range resp.Bids { ob.Bids = append(ob.Bids, &gctrpc.OrderbookItem{ @@ -911,7 +911,7 @@ func (s *RPCServer) GetOrders(_ context.Context, r *gctrpc.GetOrdersRequest) (*g Total: resp[x].Trades[i].Total, } if !resp[x].Trades[i].Timestamp.IsZero() { - t.CreationTime = resp[x].Trades[i].Timestamp.Unix() + t.CreationTime = s.unixTimestamp(resp[x].Trades[i].Timestamp) } trades = append(trades, t) } @@ -933,10 +933,10 @@ func (s *RPCServer) GetOrders(_ context.Context, r *gctrpc.GetOrdersRequest) (*g Trades: trades, } if !resp[x].Date.IsZero() { - o.CreationTime = resp[x].Date.Unix() + o.CreationTime = s.unixTimestamp(resp[x].Date) } if !resp[x].LastUpdated.IsZero() { - o.UpdateTime = resp[x].LastUpdated.Unix() + o.UpdateTime = s.unixTimestamp(resp[x].LastUpdated) } orders = append(orders, o) } @@ -995,7 +995,7 @@ func (s *RPCServer) GetManagedOrders(_ context.Context, r *gctrpc.GetOrdersReque Total: resp[x].Trades[i].Total, } if !resp[x].Trades[i].Timestamp.IsZero() { - t.CreationTime = resp[x].Trades[i].Timestamp.Unix() + t.CreationTime = s.unixTimestamp(resp[x].Trades[i].Timestamp) } trades = append(trades, t) } @@ -1017,10 +1017,10 @@ func (s *RPCServer) GetManagedOrders(_ context.Context, r *gctrpc.GetOrdersReque Trades: trades, } if !resp[x].Date.IsZero() { - o.CreationTime = resp[x].Date.Unix() + o.CreationTime = s.unixTimestamp(resp[x].Date) } if !resp[x].LastUpdated.IsZero() { - o.UpdateTime = resp[x].LastUpdated.Unix() + o.UpdateTime = s.unixTimestamp(resp[x].LastUpdated) } orders = append(orders, o) } @@ -1062,7 +1062,7 @@ func (s *RPCServer) GetOrder(_ context.Context, r *gctrpc.GetOrderRequest) (*gct var trades []*gctrpc.TradeHistory for i := range result.Trades { trades = append(trades, &gctrpc.TradeHistory{ - CreationTime: result.Trades[i].Timestamp.Unix(), + CreationTime: s.unixTimestamp(result.Trades[i].Timestamp), Id: result.Trades[i].TID, Price: result.Trades[i].Price, Amount: result.Trades[i].Amount, @@ -1075,11 +1075,11 @@ func (s *RPCServer) GetOrder(_ context.Context, r *gctrpc.GetOrderRequest) (*gct } var creationTime, updateTime int64 - if result.Date.Unix() > 0 { - creationTime = result.Date.Unix() + if !result.Date.IsZero() { + creationTime = s.unixTimestamp(result.Date) } - if result.LastUpdated.Unix() > 0 { - updateTime = result.LastUpdated.Unix() + if !result.LastUpdated.IsZero() { + updateTime = s.unixTimestamp(result.LastUpdated) } return &gctrpc.OrderDetails{ @@ -1924,7 +1924,7 @@ func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gct Base: t.Pair.Base.String(), Quote: t.Pair.Quote.String(), Delimiter: t.Pair.Delimiter}, - LastUpdated: t.LastUpdated.Unix(), + LastUpdated: s.unixTimestamp(t.LastUpdated), Last: t.Last, High: t.High, Low: t.Low, @@ -1969,7 +1969,7 @@ func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamReq Base: t.Pair.Base.String(), Quote: t.Pair.Quote.String(), Delimiter: t.Pair.Delimiter}, - LastUpdated: t.LastUpdated.Unix(), + LastUpdated: s.unixTimestamp(t.LastUpdated), Last: t.Last, High: t.High, Low: t.Low, @@ -3656,3 +3656,12 @@ func (s *RPCServer) GetDataHistoryJobSummary(_ context.Context, r *gctrpc.GetDat ResultSummaries: job.ResultRanges, }, nil } + +// unixTimestamp returns given time in either unix seconds or unix nanoseconds, depending +// on the remoteControl/gRPC/timeInNanoSeconds boolean configuration. +func (s *RPCServer) unixTimestamp(x time.Time) int64 { + if s.Config.RemoteControl.GRPC.TimeInNanoSeconds { + return x.UnixNano() + } + return x.Unix() +} diff --git a/engine/rpcserver_test.go b/engine/rpcserver_test.go index abbb77c2..84a8d297 100644 --- a/engine/rpcserver_test.go +++ b/engine/rpcserver_test.go @@ -30,6 +30,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/binance" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/gctrpc" "github.com/thrasher-corp/gocryptotrader/portfolio/banking" @@ -1688,3 +1689,96 @@ func TestGetManagedOrders(t *testing.T) { t.Errorf("unexpected order result: %v", oo) } } + +func TestRPCServer_unixTimestamp(t *testing.T) { + t.Parallel() + + s := RPCServer{ + Engine: &Engine{ + Config: &config.Config{ + RemoteControl: config.RemoteControlConfig{ + GRPC: config.GRPCConfig{ + TimeInNanoSeconds: false, + }, + }, + }, + }, + } + const sec = 1618888141 + const nsec = 2 + x := time.Unix(sec, nsec) + + timestampSeconds := s.unixTimestamp(x) + if timestampSeconds != sec { + t.Errorf("have %d, want %d", timestampSeconds, sec) + } + + s.Config.RemoteControl.GRPC.TimeInNanoSeconds = true + timestampNanos := s.unixTimestamp(x) + if want := int64(sec*1_000_000_000 + nsec); timestampNanos != want { + t.Errorf("have %d, want %d", timestampSeconds, want) + } +} + +func TestRPCServer_GetTicker_LastUpdatedNanos(t *testing.T) { + // Make a dummy pair we'll be using for this test. + pair := currency.NewPairWithDelimiter("XXXXX", "YYYYY", "") + + // Create a mock-up RPCServer and add our newly made pair to its list of available + // and enabled pairs. + server := RPCServer{Engine: RPCTestSetup(t)} + b := server.ExchangeManager.GetExchangeByName(testExchange).GetBase() + b.CurrencyPairs.Pairs[asset.Spot].Available = append( + b.CurrencyPairs.Pairs[asset.Spot].Available, + pair, + ) + b.CurrencyPairs.Pairs[asset.Spot].Enabled = append( + b.CurrencyPairs.Pairs[asset.Spot].Enabled, + pair, + ) + + // Push a mock-up ticker. + now := time.Now() + ticker.ProcessTicker(&ticker.Price{ + ExchangeName: testExchange, + Pair: pair, + AssetType: asset.Spot, + LastUpdated: now, + + Open: 69, + High: 96, + Low: 169, + Close: 196, + }) + + // Prepare a ticker request. + request := &gctrpc.GetTickerRequest{ + Exchange: testExchange, + Pair: &gctrpc.CurrencyPair{ + Delimiter: pair.Delimiter, + Base: pair.Base.String(), + Quote: pair.Quote.String(), + }, + AssetType: asset.Spot.String(), + } + + // Check if timestamp returned is in seconds if !TimeInNanoSeconds. + server.Config.RemoteControl.GRPC.TimeInNanoSeconds = false + one, err := server.GetTicker(context.Background(), request) + if err != nil { + t.Error(err) + } + if want := now.Unix(); one.LastUpdated != want { + t.Errorf("have %d, want %d", one.LastUpdated, want) + } + + // Check if timestamp returned is in nanoseconds if TimeInNanoSeconds. + server.Config.RemoteControl.GRPC.TimeInNanoSeconds = true + two, err := server.GetTicker(context.Background(), request) + if err != nil { + t.Error(err) + } + if want := now.UnixNano(); two.LastUpdated != want { + t.Errorf("have %d, want %d", two.LastUpdated, want) + } +}