Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/ExchangeSharp/API/Common/APIRequestMaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ public APIRequestMaker(IAPIRequestHandler api)
/// The encoding of payload is API dependant but is typically json.</param>
/// <param name="method">Request method or null for default. Example: 'GET' or 'POST'.</param>
/// <returns>Raw response</returns>
public async Task<string> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null)
public async Task<IAPIRequestMaker.RequestResult<string>> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null)
{
await new SynchronizationContextRemover();
await api.RateLimit.WaitToProceedAsync();
Expand Down Expand Up @@ -225,7 +225,7 @@ public async Task<string> MakeRequestAsync(string url, string? baseUrl = null, D
{
response?.Dispose();
}
return responseString;
return new IAPIRequestMaker.RequestResult<string>() { Response = responseString, HTTPHeaderDate = response.Headers.Date };
}

/// <summary>
Expand Down
21 changes: 16 additions & 5 deletions src/ExchangeSharp/API/Common/BaseAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin
/// The encoding of payload is API dependant but is typically json.
/// <param name="method">Request method or null for default</param>
/// <returns>Raw response</returns>
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);
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);

/// <summary>
/// Make a JSON request to an API end point
Expand All @@ -509,17 +509,28 @@ public void LoadAPIKeysUnsecure(string publicApiKey, string privateApiKey, strin
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
/// <param name="requestMethod">Request method or null for default</param>
/// <returns>Result decoded from JSON response</returns>
public async Task<T> MakeJsonRequestAsync<T>(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? requestMethod = null)
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;

/// <summary>
/// Make a JSON request to an API end point, with full retun result
/// </summary>
/// <typeparam name="T">Type of object to parse JSON as</typeparam>
/// <param name="url">Path and query</param>
/// <param name="baseUrl">Override the base url, null for the default BaseUrl</param>
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
/// <param name="requestMethod">Request method or null for default</param>
/// <returns>full return result, including result decoded from JSON response</returns>
public async Task<IAPIRequestMaker.RequestResult<T>> MakeJsonRequestFullAsync<T>(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? requestMethod = null)
{
await new SynchronizationContextRemover();

string stringResult = await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod);
string stringResult = (await MakeRequestAsync(url, baseUrl: baseUrl, payload: payload, method: requestMethod)).Response;
T jsonResult = JsonConvert.DeserializeObject<T>(stringResult, SerializerSettings);
if (jsonResult is JToken token)
{
return (T)(object)CheckJsonResponse(token);
return new IAPIRequestMaker.RequestResult<T>() { Response = (T)(object)CheckJsonResponse(token) };
}
return jsonResult;
return new IAPIRequestMaker.RequestResult<T>() { Response = jsonResult };
}

/// <summary>
Expand Down
31 changes: 20 additions & 11 deletions src/ExchangeSharp/API/Common/IAPIRequestMaker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,26 @@ public enum RequestMakerState
/// </summary>
public interface IAPIRequestMaker
{
/// <summary>
/// Make a request to a path on the API
/// </summary>
/// <param name="url">Path and query</param>
/// <param name="baseUrl">Override the base url, null for the default BaseUrl</param>
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
/// The encoding of payload is API dependant but is typically json.</param>
/// <param name="method">Request method or null for default</param>
/// <returns>Raw response</returns>
/// <exception cref="System.Exception">Request fails</exception>
Task<string> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null);
public class RequestResult<T>
{
/// <summary> request response </summary>
public T Response { get; set; }

/// <summary> raw HTTP header date </summary>
public DateTimeOffset? HTTPHeaderDate { get; set; }
}

/// <summary>
/// Make a request to a path on the API
/// </summary>
/// <param name="url">Path and query</param>
/// <param name="baseUrl">Override the base url, null for the default BaseUrl</param>
/// <param name="payload">Payload, can be null. For private API end points, the payload must contain a 'nonce' key set to GenerateNonce value.</param>
/// The encoding of payload is API dependant but is typically json.</param>
/// <param name="method">Request method or null for default</param>
/// <returns>Raw response</returns>
/// <exception cref="System.Exception">Request fails</exception>
Task<RequestResult<string>> MakeRequestAsync(string url, string? baseUrl = null, Dictionary<string, object>? payload = null, string? method = null);

/// <summary>
/// An action to execute when a request has been made (this request and state and object (response or exception))
Expand Down
26 changes: 7 additions & 19 deletions src/ExchangeSharp/API/Exchanges/BL3P/ExchangeBL3PAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,10 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
throw new NotSupportedException($"{order.OrderType} is not supported");
}

var resultBody = await MakeRequestAsync(
var result = (await MakeJsonRequestAsync<BL3POrderAddResponse>(
$"/{order.MarketSymbol}/money/order/add",
payload: data
);

var result = JsonConvert.DeserializeObject<BL3POrderAddResponse>(resultBody)
.Except();
)).Except();

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

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

var resultBody = await MakeRequestAsync($"/{marketSymbol}/money/depth/full");

var bl3pOrderBook = JsonConvert.DeserializeObject<BL3PReponseFullOrderBook>(resultBody)
var bl3pOrderBook = (await MakeJsonRequestAsync<BL3PReponseFullOrderBook>($"/{marketSymbol}/money/depth/full"))
.Except();

bl3pOrderBook.MarketSymbol??= marketSymbol;
Expand All @@ -298,16 +293,13 @@ protected override async Task OnCancelOrderAsync(string orderId, string marketSy
throw new ArgumentException("Value cannot be null or whitespace.", nameof(marketSymbol));
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");

var resultBody = await MakeRequestAsync(
(await MakeJsonRequestAsync<BL3PEmptyResponse>(
$"/{marketSymbol}/money/order/cancel",
payload: new Dictionary<string, object>
{
{"order_id", orderId}
}
);

JsonConvert.DeserializeObject<BL3PEmptyResponse>(resultBody)
.Except();
)).Except();
}

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

var resultBody = await MakeRequestAsync(
var result = (await MakeJsonRequestAsync<BL3POrderResultResponse>(
$"/{marketSymbol}/money/order/result",
payload: data
);


var result = JsonConvert.DeserializeObject<BL3POrderResultResponse>(resultBody)
.Except();
)).Except();

return new ExchangeOrderResult
{
Expand Down
2 changes: 1 addition & 1 deletion src/ExchangeSharp/API/Exchanges/Bybit/ExchangeBybitAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private async Task<T> DoMakeJsonRequestAsync<T>(string url, string? baseUrl = nu
{
await new SynchronizationContextRemover();

string stringResult = await MakeRequestAsync(url, baseUrl, payload, requestMethod);
string stringResult = (await MakeRequestAsync(url, baseUrl, payload, requestMethod)).Response;
return JsonConvert.DeserializeObject<T>(stringResult);
}
#nullable disable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -738,8 +738,10 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
}

order.ExtraParameters.CopyTo(payload);
JToken result = await MakeJsonRequestAsync<JToken>("/orders", null, payload, "POST");
return ParseOrder(result);
var result = await MakeJsonRequestFullAsync<JToken>("/orders", null, payload, "POST");
var resultOrder = ParseOrder(result.Response);
resultOrder.HTTPHeaderDate = result.HTTPHeaderDate.Value.UtcDateTime;
return resultOrder;
}

protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS

try
{
string html = await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com");
string html = (await RequestMaker.MakeRequestAsync("/rest-api", "https://docs.gemini.com")).Response;
int startPos = html.IndexOf("<h1 id=\"symbols-and-minimums\">Symbols and minimums</h1>");
if (startPos < 0)
{
Expand Down
8 changes: 7 additions & 1 deletion src/ExchangeSharp/Model/ExchangeOrderResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ public sealed class ExchangeOrderResult
/// </summary>
public decimal Amount { get; set; }

/// <summary>Amount filled in the market currency. May be null if not provided by exchange</summary>
/// <summary>
/// In the market currency. May be null if not provided by exchange
/// 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.
/// </summary>
public decimal? AmountFilled { get; set; }

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

/// <summary> raw HTTP header date </summary>
public DateTime? HTTPHeaderDate { get; set; }

/// <summary>datetime in UTC order was completed (could be filled, cancelled, expired, rejected, etc...). Null if still open.</summary>
public DateTime? CompletedDate { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion tests/ExchangeSharpTests/ExchangeBinanceAPITests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public async Task CurrenciesParsedCorrectly()
var requestMaker = Substitute.For<IAPIRequestMaker>();
var binance = await ExchangeAPI.GetExchangeAPIAsync<ExchangeBinanceAPI>();
binance.RequestMaker = requestMaker;
requestMaker.MakeRequestAsync("/capital/config/getall", ((ExchangeBinanceAPI)binance).BaseUrlSApi).Returns(Resources.BinanceGetAllAssets);
requestMaker.MakeRequestAsync("/capital/config/getall", ((ExchangeBinanceAPI)binance).BaseUrlSApi).Returns(new IAPIRequestMaker.RequestResult<string>() { Response = Resources.BinanceGetAllAssets });
IReadOnlyDictionary<string, ExchangeCurrency> currencies = await binance.GetCurrenciesAsync();
currencies.Should().HaveCount(3);
currencies.TryGetValue("bnb", out ExchangeCurrency bnb).Should().BeTrue();
Expand Down
10 changes: 5 additions & 5 deletions tests/ExchangeSharpTests/MockAPIRequestMaker.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -31,26 +31,26 @@ public class MockAPIRequestMaker : IAPIRequestMaker
/// <param name="payload"></param>
/// <param name="method"></param>
/// <returns></returns>
public async Task<string> MakeRequestAsync(string url, string baseUrl = null, Dictionary<string, object> payload = null, string method = null)
public async Task<IAPIRequestMaker.RequestResult<string>> MakeRequestAsync(string url, string baseUrl = null, Dictionary<string, object> payload = null, string method = null)
{
await new SynchronizationContextRemover();
RequestStateChanged?.Invoke(this, RequestMakerState.Begin, null);
if (GlobalResponse != null)
{
RequestStateChanged?.Invoke(this, RequestMakerState.Finished, GlobalResponse);
return GlobalResponse;
return new IAPIRequestMaker.RequestResult<string>() { Response = GlobalResponse };
}
else if (UrlAndResponse.TryGetValue(url, out object response))
{
if (!(response is Exception ex))
{
RequestStateChanged?.Invoke(this, RequestMakerState.Finished, response as string);
return response as string;
return new IAPIRequestMaker.RequestResult<string>() { Response = response as string };
}
RequestStateChanged?.Invoke(this, RequestMakerState.Error, ex);
throw ex;
}
return @"{ ""error"": ""No result from server"" }";
return new IAPIRequestMaker.RequestResult<string>() { Response = @"{ ""error"": ""No result from server"" }" };
}
}
}