Skip to content

Commit 7df21d1

Browse files
authored
provide HTTPHeaderDate when placing Coinbase orders (#771)
1 parent 846171d commit 7df21d1

File tree

10 files changed

+64
-48
lines changed

10 files changed

+64
-48
lines changed

src/ExchangeSharp/API/Common/APIRequestMaker.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public APIRequestMaker(IAPIRequestHandler api)
162162
/// The encoding of payload is API dependant but is typically json.</param>
163163
/// <param name="method">Request method or null for default. Example: 'GET' or 'POST'.</param>
164164
/// <returns>Raw response</returns>
165-
public async Task<string> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null)
165+
public async Task<IAPIRequestMaker.RequestResult<string>> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null)
166166
{
167167
await new SynchronizationContextRemover();
168168
await api.RateLimit.WaitToProceedAsync();
@@ -225,7 +225,7 @@ public async Task<string> MakeRequestAsync(string url, string? baseUrl = null, D
225225
{
226226
response?.Dispose();
227227
}
228-
return responseString;
228+
return new IAPIRequestMaker.RequestResult<string>() { Response = responseString, HTTPHeaderDate = response.Headers.Date };
229229
}
230230

231231
/// <summary>

src/ExchangeSharp/API/Common/BaseAPI.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin
498498
/// The encoding of payload is API dependant but is typically json.
499499
/// <param name="method">Request method or null for default</param>
500500
/// <returns>Raw response</returns>
501-
public Task<string> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null) => requestMaker.MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: method);
501+
public async Task<IAPIRequestMaker.RequestResult<string>> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null) => await requestMaker.MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: method);
502502

503503
/// <summary>
504504
/// Make a JSON request to an API end point
@@ -509,17 +509,28 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin
509509
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
510510
/// <param name="requestMethod">Request method or null for default</param>
511511
/// <returns>Result decoded from JSON response</returns>
512-
public async Task<T> MakeJsonRequestAsync<T>(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? requestMethod = null)
512+
public async Task<T> MakeJsonRequestAsync<T>(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? requestMethod = null) => (await MakeJsonRequestFullAsync<T>(url, baseUrl: baseUrl, payload: payload, requestMethod: requestMethod)).Response;
513+
514+
/// <summary>
515+
/// Make a JSON request to an API end point, with full retun result
516+
/// </summary>
517+
/// <typeparam name="T">Type of object to parse JSON as</typeparam>
518+
/// <param name="url">Path and query</param>
519+
/// <param name="baseUrl">Override the base url, null for the default BaseUrl</param>
520+
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
521+
/// <param name="requestMethod">Request method or null for default</param>
522+
/// <returns>full return result, including result decoded from JSON response</returns>
523+
public async Task<IAPIRequestMaker.RequestResult<T>> MakeJsonRequestFullAsync<T>(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? requestMethod = null)
513524
{
514525
await new SynchronizationContextRemover();
515526

516-
string stringResult = await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod);
527+
string stringResult = (await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod)).Response;
517528
T jsonResult = JsonConvert.DeserializeObject<T>(stringResult, SerializerSettings);
518529
if (jsonResult is JToken token)
519530
{
520-
return (T)(object)CheckJsonResponse(token);
531+
return new IAPIRequestMaker.RequestResult<T>() { Response = (T)(object)CheckJsonResponse(token) };
521532
}
522-
return jsonResult;
533+
return new IAPIRequestMaker.RequestResult<T>() { Response = jsonResult };
523534
}
524535

525536
/// <summary>

src/ExchangeSharp/API/Common/IAPIRequestMaker.cs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,26 @@ public enum RequestMakerState
4343
/// </summary>
4444
public interface IAPIRequestMaker
4545
{
46-
/// <summary>
47-
/// Make a request to a path on the API
48-
/// </summary>
49-
/// <param name="url">Path and query</param>
50-
/// <param name="baseUrl">Override the base url, null for the default BaseUrl</param>
51-
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
52-
/// The encoding of payload is API dependant but is typically json.</param>
53-
/// <param name="method">Request method or null for default</param>
54-
/// <returns>Raw response</returns>
55-
/// <exception cref="System.Exception">Request fails</exception>
56-
Task<string> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null);
46+
public class RequestResult<T>
47+
{
48+
/// <summary> request response </summary>
49+
public T Response { get; set; }
50+
51+
/// <summary> raw HTTP header date </summary>
52+
public DateTimeOffset? HTTPHeaderDate { get; set; }
53+
}
54+
55+
/// <summary>
56+
/// Make a request to a path on the API
57+
/// </summary>
58+
/// <param name="url">Path and query</param>
59+
/// <param name="baseUrl">Override the base url, null for the default BaseUrl</param>
60+
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
61+
/// The encoding of payload is API dependant but is typically json.</param>
62+
/// <param name="method">Request method or null for default</param>
63+
/// <returns>Raw response</returns>
64+
/// <exception cref="System.Exception">Request fails</exception>
65+
Task<RequestResult<string>> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null);
5766

5867
/// <summary>
5968
/// An action to execute when a request has been made (this request and state and object (response or exception))

src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,10 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
264264
throw new NotSupportedException($"{order.OrderType} is not supported");
265265
}
266266

267-
var resultBody = await MakeRequestAsync(
267+
var result = (await MakeJsonRequestAsync<BL3POrderAddResponse>(
268268
$"/{order.MarketSymbol}/money/order/add",
269269
payload: data
270-
);
271-
272-
var result = JsonConvert.DeserializeObject<BL3POrderAddResponse>(resultBody)
273-
.Except();
270+
)).Except();
274271

275272
var orderDetails = await GetOrderDetailsAsync(result.OrderId, marketSymbol: order.MarketSymbol);
276273

@@ -282,9 +279,7 @@ protected override async Task<ExchangeOrderBook> OnGetOrderBookAsync(string mark
282279
if (string.IsNullOrWhiteSpace(marketSymbol))
283280
throw new ArgumentException("Value cannot be null or whitespace.", nameof(marketSymbol));
284281

285-
var resultBody = await MakeRequestAsync($"/{marketSymbol}/money/depth/full");
286-
287-
var bl3pOrderBook = JsonConvert.DeserializeObject<BL3PReponseFullOrderBook>(resultBody)
282+
var bl3pOrderBook = (await MakeJsonRequestAsync<BL3PReponseFullOrderBook>($"/{marketSymbol}/money/depth/full"))
288283
.Except();
289284

290285
bl3pOrderBook.MarketSymbol??= marketSymbol;
@@ -298,16 +293,13 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy
298293
throw new ArgumentException("Value cannot be null or whitespace.", nameof(marketSymbol));
299294
if (isClientOrderId) throw new NotSupportedException("Cancelling by client order ID is not supported in ExchangeSharp. Please submit a PR if you are interested in this feature");
300295

301-
var resultBody = await MakeRequestAsync(
296+
(await MakeJsonRequestAsync<BL3PEmptyResponse>(
302297
$"/{marketSymbol}/money/order/cancel",
303298
payload: new Dictionary<string, object>
304299
{
305300
{"order_id", orderId}
306301
}
307-
);
308-
309-
JsonConvert.DeserializeObject<BL3PEmptyResponse>(resultBody)
310-
.Except();
302+
)).Except();
311303
}
312304

313305
protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
@@ -321,14 +313,10 @@ protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string
321313
{"order_id", orderId}
322314
};
323315

324-
var resultBody = await MakeRequestAsync(
316+
var result = (await MakeJsonRequestAsync<BL3POrderResultResponse>(
325317
$"/{marketSymbol}/money/order/result",
326318
payload: data
327-
);
328-
329-
330-
var result = JsonConvert.DeserializeObject<BL3POrderResultResponse>(resultBody)
331-
.Except();
319+
)).Except();
332320

333321
return new ExchangeOrderResult
334322
{

src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ private async Task<T> DoMakeJsonRequestAsync<T>(string url, string? baseUrl = nu
8181
{
8282
await new SynchronizationContextRemover();
8383

84-
string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod);
84+
string stringResult = (await MakeRequestAsync(url, baseUrl, payload, requestMethod)).Response;
8585
return JsonConvert.DeserializeObject<T>(stringResult);
8686
}
8787
#nullable disable

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -738,8 +738,10 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
738738
}
739739

740740
order.ExtraParameters.CopyTo(payload);
741-
JToken result = await MakeJsonRequestAsync<JToken>("/orders", null, payload, "POST");
742-
return ParseOrder(result);
741+
var result = await MakeJsonRequestFullAsync<JToken>("/orders", null, payload, "POST");
742+
var resultOrder = ParseOrder(result.Response);
743+
resultOrder.HTTPHeaderDate = result.HTTPHeaderDate.Value.UtcDateTime;
744+
return resultOrder;
743745
}
744746

745747
protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)

src/ExchangeSharp/API/Exchanges/Gemini/ExchangeGeminiAPI.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
102102

103103
try
104104
{
105-
string html = await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com");
105+
string html = (await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com")).Response;
106106
int startPos = html.IndexOf("<h1 id=\"symbols-and-minimums\">Symbols and minimums</h1>");
107107
if (startPos < 0)
108108
{

src/ExchangeSharp/Model/ExchangeOrderResult.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ public sealed class ExchangeOrderResult
4646
/// </summary>
4747
public decimal Amount { get; set; }
4848

49-
/// <summary>Amount filled in the market currency. May be null if not provided by exchange</summary>
49+
/// <summary>
50+
/// In the market currency. May be null if not provided by exchange
51+
/// If this is a Trade, then this is the amount filled in the trade. If it is an order, this is the cumulative amount filled in the order so far.
52+
/// </summary>
5053
public decimal? AmountFilled { get; set; }
5154

5255
/// <summary>
@@ -67,6 +70,9 @@ public sealed class ExchangeOrderResult
6770
/// <summary>Order datetime in UTC</summary>
6871
public DateTime OrderDate { get; set; }
6972

73+
/// <summary> raw HTTP header date </summary>
74+
public DateTime? HTTPHeaderDate { get; set; }
75+
7076
/// <summary>datetime in UTC order was completed (could be filled, cancelled, expired, rejected, etc...). Null if still open.</summary>
7177
public DateTime? CompletedDate { get; set; }
7278

tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public async Task CurrenciesParsedCorrectly()
102102
var requestMaker = Substitute.For<IAPIRequestMaker>();
103103
var binance = await ExchangeAPI.GetExchangeAPIAsync<ExchangeBinanceAPI>();
104104
binance.RequestMaker = requestMaker;
105-
requestMaker.MakeRequestAsync("/capital/config/getall", ((ExchangeBinanceAPI)binance).BaseUrlSApi).Returns(Resources.BinanceGetAllAssets);
105+
requestMaker.MakeRequestAsync("/capital/config/getall", ((ExchangeBinanceAPI)binance).BaseUrlSApi).Returns(new IAPIRequestMaker.RequestResult<string>() { Response = Resources.BinanceGetAllAssets });
106106
IReadOnlyDictionary<string, ExchangeCurrency> currencies = await binance.GetCurrenciesAsync();
107107
currencies.Should().HaveCount(3);
108108
currencies.TryGetValue("bnb", out ExchangeCurrency bnb).Should().BeTrue();

tests/ExchangeSharpTests/MockAPIRequestMaker.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.Text;
44
using System.Threading.Tasks;
@@ -31,26 +31,26 @@ public class MockAPIRequestMaker : IAPIRequestMaker
3131
/// <param name="payload"></param>
3232
/// <param name="method"></param>
3333
/// <returns></returns>
34-
public async Task<string> MakeRequestAsync(string url, string baseUrl = null, Dictionary<string, object> payload = null, string method = null)
34+
public async Task<IAPIRequestMaker.RequestResult<string>> MakeRequestAsync(string url, string baseUrl = null, Dictionary<string, object> payload = null, string method = null)
3535
{
3636
await new SynchronizationContextRemover();
3737
RequestStateChanged?.Invoke(this, RequestMakerState.Begin, null);
3838
if (GlobalResponse != null)
3939
{
4040
RequestStateChanged?.Invoke(this, RequestMakerState.Finished, GlobalResponse);
41-
return GlobalResponse;
41+
return new IAPIRequestMaker.RequestResult<string>() { Response = GlobalResponse };
4242
}
4343
else if (UrlAndResponse.TryGetValue(url, out object response))
4444
{
4545
if (!(response is Exception ex))
4646
{
4747
RequestStateChanged?.Invoke(this, RequestMakerState.Finished, response as string);
48-
return response as string;
48+
return new IAPIRequestMaker.RequestResult<string>() { Response = response as string };
4949
}
5050
RequestStateChanged?.Invoke(this, RequestMakerState.Error, ex);
5151
throw ex;
5252
}
53-
return @"{ ""error"": ""No result from server"" }";
53+
return new IAPIRequestMaker.RequestResult<string>() { Response = @"{ ""error"": ""No result from server"" }" };
5454
}
5555
}
5656
}

0 commit comments

Comments
 (0)