From 726244ddc491fa16704b30cdfbcd92ee81920983 Mon Sep 17 00:00:00 2001 From: Victor Lee Date: Thu, 31 Mar 2022 18:03:02 -0700 Subject: [PATCH] fixed JSON deserialization in Coinbase with new SerializerSettings - added test for Coinbase Match - fixed compilation error in ExchangeBinanceAPITests.cs --- src/ExchangeSharp/API/Common/BaseAPI.cs | 13 ++++++- .../Exchanges/Coinbase/ExchangeCoinbaseAPI.cs | 24 ++++++------ .../Coinbase/Models/Response/Messages.cs | 8 ++-- .../API/Exchanges/_Base/ExchangeAPI.cs | 1 - .../ExchangeBinanceAPITests.cs | 11 +++--- .../ExchangeCoinbaseAPITests.cs | 39 +++++++++++++++++++ tests/ExchangeSharpTests/ExchangeTests.cs | 2 +- 7 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 tests/ExchangeSharpTests/ExchangeCoinbaseAPITests.cs diff --git a/src/ExchangeSharp/API/Common/BaseAPI.cs b/src/ExchangeSharp/API/Common/BaseAPI.cs index 58e3e4c6..12b0e9e8 100644 --- a/src/ExchangeSharp/API/Common/BaseAPI.cs +++ b/src/ExchangeSharp/API/Common/BaseAPI.cs @@ -29,6 +29,7 @@ The above copyright notice and this permission notice shall be included in all c using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; namespace ExchangeSharp { @@ -246,6 +247,16 @@ public RateGate RateLimit /// public Dictionary MethodCachePolicy { get; } = new Dictionary(); + public static JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings + { + FloatParseHandling = FloatParseHandling.Decimal, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }, + }; + private ICache cache = new MemoryCache(); /// /// Get or set the current cache. Defaults to MemoryCache. @@ -503,7 +514,7 @@ public async Task MakeJsonRequestAsync(string url, string? baseUrl = null, await new SynchronizationContextRemover(); string stringResult = await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod); - T jsonResult = JsonConvert.DeserializeObject(stringResult); + T jsonResult = JsonConvert.DeserializeObject(stringResult, SerializerSettings); if (jsonResult is JToken token) { return (T)(object)CheckJsonResponse(token); diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 8d28fe0e..4f8d32b6 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -316,7 +316,7 @@ protected override Task OnGetDeltaOrderBookWebSocketAsync(Action(message); + var delta = JsonConvert.DeserializeObject(message, SerializerSettings); book.MarketSymbol = delta.ProductId; book.SequenceId = delta.Time.Ticks; foreach (string[] change in delta.Changes) @@ -336,7 +336,7 @@ protected override Task OnGetDeltaOrderBookWebSocketAsync(Action(message); + var snapshot = JsonConvert.DeserializeObject(message, SerializerSettings); book.MarketSymbol = snapshot.ProductId; foreach (decimal[] ask in snapshot.Asks) { @@ -451,11 +451,11 @@ protected override async Task OnUserDataWebSocketAsync(Action { var token = msg.ToStringFromUTF8(); - var response = JsonConvert.DeserializeObject(token); + var response = JsonConvert.DeserializeObject(token, SerializerSettings); switch (response.Type) { case ResponseType.Subscriptions: - var subscription = JsonConvert.DeserializeObject(token); + var subscription = JsonConvert.DeserializeObject(token, SerializerSettings); if (subscription.Channels == null || !subscription.Channels.Any()) { Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() no channels subscribed"); @@ -473,37 +473,37 @@ protected override async Task OnUserDataWebSocketAsync(Action(token); + var heartbeat = JsonConvert.DeserializeObject(token, SerializerSettings); Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() heartbeat received {heartbeat}"); break; case ResponseType.Received: - var received = JsonConvert.DeserializeObject(token); + var received = JsonConvert.DeserializeObject(token, SerializerSettings); callback(received.ExchangeOrderResult); break; case ResponseType.Open: - var open = JsonConvert.DeserializeObject(token); + var open = JsonConvert.DeserializeObject(token, SerializerSettings); callback(open.ExchangeOrderResult); break; case ResponseType.Done: - var done = JsonConvert.DeserializeObject(token); + var done = JsonConvert.DeserializeObject(token, SerializerSettings); callback(done.ExchangeOrderResult); break; case ResponseType.Match: - var match = JsonConvert.DeserializeObject(token); + var match = JsonConvert.DeserializeObject(token, SerializerSettings); callback(match.ExchangeOrderResult); break; case ResponseType.LastMatch: //var lastMatch = JsonConvert.DeserializeObject(token); throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()"); case ResponseType.Error: - var error = JsonConvert.DeserializeObject(token); + var error = JsonConvert.DeserializeObject(token, SerializerSettings); throw new APIException($"{error.Reason}: {error.Message}"); case ResponseType.Change: - var change = JsonConvert.DeserializeObject(token); + var change = JsonConvert.DeserializeObject(token, SerializerSettings); callback(change.ExchangeOrderResult); break; case ResponseType.Activate: - var activate = JsonConvert.DeserializeObject(token); + var activate = JsonConvert.DeserializeObject(token, SerializerSettings); callback(activate.ExchangeOrderResult); break; case ResponseType.Status: diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs index c4b5d940..969b9c29 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs @@ -40,7 +40,7 @@ internal class Activate : BaseMessage OrderId = OrderId.ToString(), ClientOrderId = null, // not provided here Result = ExchangeAPIOrderResult.PendingOpen, // order has just been activated (so it starts in PendingOpen) - Message = null, // can use for something in the future if needed + Message = null, // + can use for something in the future if needed Amount = Size, AmountFilled = 0, // just activated, so none filled Price = null, // not specified here (only StopPrice is) @@ -78,7 +78,7 @@ internal class Change : BaseMessage AmountFilled = null, // not specified here Price = Price, AveragePrice = null, // not specified here - // OrderDate - unclear if the Time in the Change msg is the new OrderDate or whether that is unchanged + OrderDate = Time, // + unclear if the Time in the Change msg is the new OrderDate or whether that is unchanged CompletedDate = null, // order is active MarketSymbol = ProductId, IsBuy = Side == OrderSide.Buy, @@ -132,7 +132,7 @@ internal class Heartbeat : BaseMessage public long LastTradeId { get; set; } public string ProductId { get; set; } public long Sequence { get; set; } - public System.DateTimeOffset Time { get; set; } + public DateTimeOffset Time { get; set; } public override string ToString() { return $"Heartbeat: Last TID {LastTradeId}, Product Id {ProductId}, Sequence {Sequence}, Time {Time}"; @@ -185,7 +185,7 @@ internal class Match : BaseMessage AveragePrice = Price, // not specified here // OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable CompletedDate = null, // order not necessarily fullly filled at this point - TradeDate = Time.ToDateTimeInvariant(), + TradeDate = Time.UtcDateTime, MarketSymbol = ProductId, IsBuy = Side == OrderSide.Buy, Fees = (MakerFeeRate ?? TakerFeeRate) * Price * Size, diff --git a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs index 85f9d69e..b01a70cf 100644 --- a/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs @@ -15,7 +15,6 @@ The above copyright notice and this permission notice shall be included in all c using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; diff --git a/tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs b/tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs index 999d5357..f0d7f3e0 100644 --- a/tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs +++ b/tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs @@ -52,7 +52,7 @@ public void DeserializeDiff() ] ] }"; - var diff = JsonConvert.DeserializeObject(toParse); + var diff = JsonConvert.DeserializeObject(toParse, BaseAPI.SerializerSettings); ValidateDiff(diff); } @@ -82,7 +82,7 @@ public void DeserializeComboStream() } }"; - var multistream = JsonConvert.DeserializeObject(toParse); + var multistream = JsonConvert.DeserializeObject(toParse, BaseAPI.SerializerSettings); multistream.Stream.Should().Be("bnbbtc@depth"); ValidateDiff(multistream.Data); } @@ -92,7 +92,7 @@ public void DeserializeRealData() { string real = "{\"stream\":\"bnbbtc@depth\",\"data\":{\"e\":\"depthUpdate\",\"E\":1527540113575,\"s\":\"BNBBTC\",\"U\":77730662,\"u\":77730663,\"b\":[[\"0.00167300\",\"0.00000000\",[]],[\"0.00165310\",\"16.44000000\",[]]],\"a\":[]}}"; - var diff = JsonConvert.DeserializeObject(real); + var diff = JsonConvert.DeserializeObject(real, BaseAPI.SerializerSettings); diff.Data.EventTime.Should().Be(1527540113575); } @@ -100,8 +100,9 @@ public void DeserializeRealData() public async Task CurrenciesParsedCorrectly() { var requestMaker = Substitute.For(); - requestMaker.MakeRequestAsync("/capital/config/getall", new ExchangeBinanceAPI().BaseUrlSApi).Returns(Resources.BinanceGetAllAssets); - var binance = new ExchangeBinanceAPI { RequestMaker = requestMaker }; + var binance = await ExchangeAPI.GetExchangeAPIAsync(); + binance.RequestMaker = requestMaker; + requestMaker.MakeRequestAsync("/capital/config/getall", ((ExchangeBinanceAPI)binance).BaseUrlSApi).Returns(Resources.BinanceGetAllAssets); IReadOnlyDictionary currencies = await binance.GetCurrenciesAsync(); currencies.Should().HaveCount(3); currencies.TryGetValue("bnb", out ExchangeCurrency bnb).Should().BeTrue(); diff --git a/tests/ExchangeSharpTests/ExchangeCoinbaseAPITests.cs b/tests/ExchangeSharpTests/ExchangeCoinbaseAPITests.cs new file mode 100644 index 00000000..3c0b55f2 --- /dev/null +++ b/tests/ExchangeSharpTests/ExchangeCoinbaseAPITests.cs @@ -0,0 +1,39 @@ +using ExchangeSharp; +using ExchangeSharp.Coinbase; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ExchangeSharpTests +{ + [TestClass] + public sealed class ExchangeCoinbaseAPITests + { + private async Task MakeMockRequestMaker(string response = null) + { + var requestMaker = new MockAPIRequestMaker(); + if (response != null) + { + requestMaker.GlobalResponse = response; + } + var api = (await ExchangeAPI.GetExchangeAPIAsync(ExchangeName.Coinbase) as ExchangeCoinbaseAPI)!; + api.RequestMaker = requestMaker; + return api; + } + + [TestMethod] + public void DeserializeUserMatch() + { + string toParse = "{\r\n \"type\": \"match\",\r\n \"trade_id\": 10,\r\n \"sequence\": 50,\r\n \"maker_order_id\": \"ac928c66-ca53-498f-9c13-a110027a60e8\",\r\n \"taker_order_id\": \"132fb6ae-456b-4654-b4e0-d681ac05cea1\",\r\n \"time\": \"2014-11-07T08:19:27.028459Z\",\r\n \"product_id\": \"BTC-USD\",\r\n \"size\": \"5.23512\",\r\n \"price\": \"400.23\",\r\n \"side\": \"sell\"\r\n,\r\n \"taker_user_id\": \"5844eceecf7e803e259d0365\",\r\n \"user_id\": \"5844eceecf7e803e259d0365\",\r\n \"taker_profile_id\": \"765d1549-9660-4be2-97d4-fa2d65fa3352\",\r\n \"profile_id\": \"765d1549-9660-4be2-97d4-fa2d65fa3352\",\r\n \"taker_fee_rate\": \"0.005\"\r\n}"; + + var usermatch = JsonConvert.DeserializeObject(toParse, BaseAPI.SerializerSettings); + usermatch.MakerOrderId.Should().Be("ac928c66-ca53-498f-9c13-a110027a60e8"); + usermatch.ExchangeOrderResult.OrderId.Should().Be("132fb6ae-456b-4654-b4e0-d681ac05cea1"); + } + } +} diff --git a/tests/ExchangeSharpTests/ExchangeTests.cs b/tests/ExchangeSharpTests/ExchangeTests.cs index dbe6ed64..281bb850 100644 --- a/tests/ExchangeSharpTests/ExchangeTests.cs +++ b/tests/ExchangeSharpTests/ExchangeTests.cs @@ -91,7 +91,7 @@ public async Task GlobalSymbolTest() string globalMarketSymbol = "ETH-BTC"; //1 ETH is worth 0.0192 BTC... string globalMarketSymbolAlt = "BTC-KRW"; // WTF Bitthumb... //1 BTC worth 9,783,000 won - Dictionary allSymbols = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText("TestData/AllSymbols.json")); + Dictionary allSymbols = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText("TestData/AllSymbols.json"), ExchangeAPI.SerializerSettings); // sanity test that all exchanges return the same global symbol when converted back and forth foreach (IExchangeAPI api in await ExchangeAPI.GetExchangeAPIsAsync())