Skip to content

Commit 0eed42a

Browse files
authored
fixed JSON deserialization in Coinbase with new SerializerSettings (#757)
- added test for Coinbase Match - fixed compilation error in ExchangeBinanceAPITests.cs
1 parent 985e28a commit 0eed42a

File tree

7 files changed

+74
-24
lines changed

7 files changed

+74
-24
lines changed

src/ExchangeSharp/API/Common/BaseAPI.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The above copyright notice and this permission notice shall be included in all c
2929

3030
using Newtonsoft.Json;
3131
using Newtonsoft.Json.Linq;
32+
using Newtonsoft.Json.Serialization;
3233

3334
namespace ExchangeSharp
3435
{
@@ -246,6 +247,16 @@ public RateGate RateLimit
246247
/// </summary>
247248
public Dictionary<string, TimeSpan> MethodCachePolicy { get; } = new Dictionary<string, TimeSpan>();
248249

250+
public static JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings
251+
{
252+
FloatParseHandling = FloatParseHandling.Decimal,
253+
NullValueHandling = NullValueHandling.Ignore,
254+
ContractResolver = new DefaultContractResolver
255+
{
256+
NamingStrategy = new SnakeCaseNamingStrategy()
257+
},
258+
};
259+
249260
private ICache cache = new MemoryCache();
250261
/// <summary>
251262
/// Get or set the current cache. Defaults to MemoryCache.
@@ -503,7 +514,7 @@ public async Task<T> MakeJsonRequestAsync<T>(string url, string? baseUrl = null,
503514
await new SynchronizationContextRemover();
504515

505516
string stringResult = await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod);
506-
T jsonResult = JsonConvert.DeserializeObject<T>(stringResult);
517+
T jsonResult = JsonConvert.DeserializeObject<T>(stringResult, SerializerSettings);
507518
if (jsonResult is JToken token)
508519
{
509520
return (T)(object)CheckJsonResponse(token);

src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ protected override Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<Exc
316316
if (message.Contains(@"""l2update"""))
317317
{
318318
// parse delta update
319-
var delta = JsonConvert.DeserializeObject<Level2>(message);
319+
var delta = JsonConvert.DeserializeObject<Level2>(message, SerializerSettings);
320320
book.MarketSymbol = delta.ProductId;
321321
book.SequenceId = delta.Time.Ticks;
322322
foreach (string[] change in delta.Changes)
@@ -336,7 +336,7 @@ protected override Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<Exc
336336
else if (message.Contains(@"""snapshot"""))
337337
{
338338
// parse snapshot
339-
var snapshot = JsonConvert.DeserializeObject<Snapshot>(message);
339+
var snapshot = JsonConvert.DeserializeObject<Snapshot>(message, SerializerSettings);
340340
book.MarketSymbol = snapshot.ProductId;
341341
foreach (decimal[] ask in snapshot.Asks)
342342
{
@@ -451,11 +451,11 @@ protected override async Task<IWebSocket> OnUserDataWebSocketAsync(Action<object
451451
return await ConnectPublicWebSocketAsync("/", async (_socket, msg) =>
452452
{
453453
var token = msg.ToStringFromUTF8();
454-
var response = JsonConvert.DeserializeObject<BaseMessage>(token);
454+
var response = JsonConvert.DeserializeObject<BaseMessage>(token, SerializerSettings);
455455
switch (response.Type)
456456
{
457457
case ResponseType.Subscriptions:
458-
var subscription = JsonConvert.DeserializeObject<Subscription>(token);
458+
var subscription = JsonConvert.DeserializeObject<Subscription>(token, SerializerSettings);
459459
if (subscription.Channels == null || !subscription.Channels.Any())
460460
{
461461
Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() no channels subscribed");
@@ -473,37 +473,37 @@ protected override async Task<IWebSocket> OnUserDataWebSocketAsync(Action<object
473473
case ResponseType.L2Update:
474474
throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()");
475475
case ResponseType.Heartbeat:
476-
var heartbeat = JsonConvert.DeserializeObject<Heartbeat>(token);
476+
var heartbeat = JsonConvert.DeserializeObject<Heartbeat>(token, SerializerSettings);
477477
Trace.WriteLine($"{nameof(OnUserDataWebSocketAsync)}() heartbeat received {heartbeat}");
478478
break;
479479
case ResponseType.Received:
480-
var received = JsonConvert.DeserializeObject<Received>(token);
480+
var received = JsonConvert.DeserializeObject<Received>(token, SerializerSettings);
481481
callback(received.ExchangeOrderResult);
482482
break;
483483
case ResponseType.Open:
484-
var open = JsonConvert.DeserializeObject<Open>(token);
484+
var open = JsonConvert.DeserializeObject<Open>(token, SerializerSettings);
485485
callback(open.ExchangeOrderResult);
486486
break;
487487
case ResponseType.Done:
488-
var done = JsonConvert.DeserializeObject<Done>(token);
488+
var done = JsonConvert.DeserializeObject<Done>(token, SerializerSettings);
489489
callback(done.ExchangeOrderResult);
490490
break;
491491
case ResponseType.Match:
492-
var match = JsonConvert.DeserializeObject<Match>(token);
492+
var match = JsonConvert.DeserializeObject<Match>(token, SerializerSettings);
493493
callback(match.ExchangeOrderResult);
494494
break;
495495
case ResponseType.LastMatch:
496496
//var lastMatch = JsonConvert.DeserializeObject<LastMatch>(token);
497497
throw new NotImplementedException($"Not expecting type {response.Type} in {nameof(OnUserDataWebSocketAsync)}()");
498498
case ResponseType.Error:
499-
var error = JsonConvert.DeserializeObject<Error>(token);
499+
var error = JsonConvert.DeserializeObject<Error>(token, SerializerSettings);
500500
throw new APIException($"{error.Reason}: {error.Message}");
501501
case ResponseType.Change:
502-
var change = JsonConvert.DeserializeObject<Change>(token);
502+
var change = JsonConvert.DeserializeObject<Change>(token, SerializerSettings);
503503
callback(change.ExchangeOrderResult);
504504
break;
505505
case ResponseType.Activate:
506-
var activate = JsonConvert.DeserializeObject<Activate>(token);
506+
var activate = JsonConvert.DeserializeObject<Activate>(token, SerializerSettings);
507507
callback(activate.ExchangeOrderResult);
508508
break;
509509
case ResponseType.Status:

src/ExchangeSharp/API/Exchanges/Coinbase/Models/Response/Messages.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ internal class Activate : BaseMessage
4040
OrderId = OrderId.ToString(),
4141
ClientOrderId = null, // not provided here
4242
Result = ExchangeAPIOrderResult.PendingOpen, // order has just been activated (so it starts in PendingOpen)
43-
Message = null, // can use for something in the future if needed
43+
Message = null, // + can use for something in the future if needed
4444
Amount = Size,
4545
AmountFilled = 0, // just activated, so none filled
4646
Price = null, // not specified here (only StopPrice is)
@@ -78,7 +78,7 @@ internal class Change : BaseMessage
7878
AmountFilled = null, // not specified here
7979
Price = Price,
8080
AveragePrice = null, // not specified here
81-
// OrderDate - unclear if the Time in the Change msg is the new OrderDate or whether that is unchanged
81+
OrderDate = Time, // + unclear if the Time in the Change msg is the new OrderDate or whether that is unchanged
8282
CompletedDate = null, // order is active
8383
MarketSymbol = ProductId,
8484
IsBuy = Side == OrderSide.Buy,
@@ -132,7 +132,7 @@ internal class Heartbeat : BaseMessage
132132
public long LastTradeId { get; set; }
133133
public string ProductId { get; set; }
134134
public long Sequence { get; set; }
135-
public System.DateTimeOffset Time { get; set; }
135+
public DateTimeOffset Time { get; set; }
136136
public override string ToString()
137137
{
138138
return $"Heartbeat: Last TID {LastTradeId}, Product Id {ProductId}, Sequence {Sequence}, Time {Time}";
@@ -185,7 +185,7 @@ internal class Match : BaseMessage
185185
AveragePrice = Price, // not specified here
186186
// OrderDate - not provided here. ideally would be null but ExchangeOrderResult.OrderDate is not nullable
187187
CompletedDate = null, // order not necessarily fullly filled at this point
188-
TradeDate = Time.ToDateTimeInvariant(),
188+
TradeDate = Time.UtcDateTime,
189189
MarketSymbol = ProductId,
190190
IsBuy = Side == OrderSide.Buy,
191191
Fees = (MakerFeeRate ?? TakerFeeRate) * Price * Size,

src/ExchangeSharp/API/Exchanges/_Base/ExchangeAPI.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ The above copyright notice and this permission notice shall be included in all c
1515
using System.Collections.Generic;
1616
using System.IO;
1717
using System.Linq;
18-
using System.Text.RegularExpressions;
1918
using System.Threading;
2019
using System.Threading.Tasks;
2120

tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public void DeserializeDiff()
5252
]
5353
]
5454
}";
55-
var diff = JsonConvert.DeserializeObject<MarketDepthDiffUpdate>(toParse);
55+
var diff = JsonConvert.DeserializeObject<MarketDepthDiffUpdate>(toParse, BaseAPI.SerializerSettings);
5656
ValidateDiff(diff);
5757
}
5858

@@ -82,7 +82,7 @@ public void DeserializeComboStream()
8282
}
8383
}";
8484

85-
var multistream = JsonConvert.DeserializeObject<MultiDepthStream>(toParse);
85+
var multistream = JsonConvert.DeserializeObject<MultiDepthStream>(toParse, BaseAPI.SerializerSettings);
8686
multistream.Stream.Should().Be("bnbbtc@depth");
8787
ValidateDiff(multistream.Data);
8888
}
@@ -92,16 +92,17 @@ public void DeserializeRealData()
9292
{
9393
string real =
9494
"{\"stream\":\"bnbbtc@depth\",\"data\":{\"e\":\"depthUpdate\",\"E\":1527540113575,\"s\":\"BNBBTC\",\"U\":77730662,\"u\":77730663,\"b\":[[\"0.00167300\",\"0.00000000\",[]],[\"0.00165310\",\"16.44000000\",[]]],\"a\":[]}}";
95-
var diff = JsonConvert.DeserializeObject<MultiDepthStream>(real);
95+
var diff = JsonConvert.DeserializeObject<MultiDepthStream>(real, BaseAPI.SerializerSettings);
9696
diff.Data.EventTime.Should().Be(1527540113575);
9797
}
9898

9999
[TestMethod]
100100
public async Task CurrenciesParsedCorrectly()
101101
{
102102
var requestMaker = Substitute.For<IAPIRequestMaker>();
103-
requestMaker.MakeRequestAsync("/capital/config/getall", new ExchangeBinanceAPI().BaseUrlSApi).Returns(Resources.BinanceGetAllAssets);
104-
var binance = new ExchangeBinanceAPI { RequestMaker = requestMaker };
103+
var binance = await ExchangeAPI.GetExchangeAPIAsync<ExchangeBinanceAPI>();
104+
binance.RequestMaker = requestMaker;
105+
requestMaker.MakeRequestAsync("/capital/config/getall", ((ExchangeBinanceAPI)binance).BaseUrlSApi).Returns(Resources.BinanceGetAllAssets);
105106
IReadOnlyDictionary<string, ExchangeCurrency> currencies = await binance.GetCurrenciesAsync();
106107
currencies.Should().HaveCount(3);
107108
currencies.TryGetValue("bnb", out ExchangeCurrency bnb).Should().BeTrue();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using ExchangeSharp;
2+
using ExchangeSharp.Coinbase;
3+
using FluentAssertions;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using Newtonsoft.Json;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Text;
10+
using System.Threading.Tasks;
11+
12+
namespace ExchangeSharpTests
13+
{
14+
[TestClass]
15+
public sealed class ExchangeCoinbaseAPITests
16+
{
17+
private async Task<ExchangeCoinbaseAPI> MakeMockRequestMaker(string response = null)
18+
{
19+
var requestMaker = new MockAPIRequestMaker();
20+
if (response != null)
21+
{
22+
requestMaker.GlobalResponse = response;
23+
}
24+
var api = (await ExchangeAPI.GetExchangeAPIAsync(ExchangeName.Coinbase) as ExchangeCoinbaseAPI)!;
25+
api.RequestMaker = requestMaker;
26+
return api;
27+
}
28+
29+
[TestMethod]
30+
public void DeserializeUserMatch()
31+
{
32+
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}";
33+
34+
var usermatch = JsonConvert.DeserializeObject<Match>(toParse, BaseAPI.SerializerSettings);
35+
usermatch.MakerOrderId.Should().Be("ac928c66-ca53-498f-9c13-a110027a60e8");
36+
usermatch.ExchangeOrderResult.OrderId.Should().Be("132fb6ae-456b-4654-b4e0-d681ac05cea1");
37+
}
38+
}
39+
}

tests/ExchangeSharpTests/ExchangeTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public async Task GlobalSymbolTest()
9191

9292
string globalMarketSymbol = "ETH-BTC"; //1 ETH is worth 0.0192 BTC...
9393
string globalMarketSymbolAlt = "BTC-KRW"; // WTF Bitthumb... //1 BTC worth 9,783,000 won
94-
Dictionary<string, string[]> allSymbols = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(System.IO.File.ReadAllText("TestData/AllSymbols.json"));
94+
Dictionary<string, string[]> allSymbols = JsonConvert.DeserializeObject<Dictionary<string, string[]>>(System.IO.File.ReadAllText("TestData/AllSymbols.json"), ExchangeAPI.SerializerSettings);
9595

9696
// sanity test that all exchanges return the same global symbol when converted back and forth
9797
foreach (IExchangeAPI api in await ExchangeAPI.GetExchangeAPIsAsync())

0 commit comments

Comments
 (0)