From fdc5cfc546421446c5fa052f3e798dbe8260018f Mon Sep 17 00:00:00 2001 From: Matt O'Keefe Date: Tue, 23 Jan 2024 13:42:27 -0600 Subject: [PATCH 1/3] chore: Adding better errors for coinbase order errors --- .../Exchanges/Coinbase/ExchangeCoinbaseAPI.cs | 76 +++++++++++-------- src/ExchangeSharp/ExchangeSharp.csproj | 2 +- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 81ba7ac3..82170730 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -29,10 +29,10 @@ public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI public override string BaseUrl { get; set; } = "https://api.coinbase.com/api/v3/brokerage"; private readonly string BaseUrlV2 = "https://api.coinbase.com/v2"; // For Wallet Support public override string BaseUrlWebSocket { get; set; } = "wss://advanced-trade-ws.coinbase.com"; - + private enum PaginationType { None, V2, V3} private PaginationType pagination = PaginationType.None; - private string cursorNext; + private string cursorNext; private Dictionary Accounts = null; // Cached Account IDs @@ -62,7 +62,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob JToken token = JsonConvert.DeserializeObject((string)response); if (token == null) return; switch(pagination) - { + { case PaginationType.V2: cursorNext = token["pagination"]?["next_starting_after"]?.ToStringInvariant(); break; case PaginationType.V3: cursorNext = token[CURSOR]?.ToStringInvariant(); break; } @@ -77,7 +77,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob /// /// protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary payload) - { + { return (PrivateApiKey != null && PublicApiKey != null); } @@ -90,7 +90,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti // V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly) string path = request.RequestUri.AbsoluteUri.StartsWith(BaseUrlV2) ? request.RequestUri.PathAndQuery : request.RequestUri.LocalPath; - string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString()); + string signature = CryptoUtility.SHA256Sign(timestamp + request.Method.ToUpperInvariant() + path + body, PrivateApiKey.ToUnsecureString()); request.AddHeader("CB-ACCESS-KEY", PublicApiKey.ToUnsecureString()); request.AddHeader("CB-ACCESS-SIGN", signature); @@ -141,7 +141,7 @@ protected internal override async Task> OnGetMarketS protected override async Task> OnGetMarketSymbolsAsync() { - return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol); + return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol); } protected override async Task> OnGetCurrenciesAsync() @@ -176,7 +176,7 @@ protected override async Task> OnG currencies[currency.Name] = currency; } } - return currencies; + return currencies; } protected override async Task>> OnGetTickersAsync() @@ -187,7 +187,7 @@ protected override async Task>> foreach (JToken book in books[PRICEBOOKS]) { var split = book[PRODUCTID].ToString().Split(GlobalMarketSymbolSeparator); - // This endpoint does not provide a last or open for the ExchangeTicker + // This endpoint does not provide a last or open for the ExchangeTicker tickers.Add(new KeyValuePair(book[PRODUCTID].ToString(), new ExchangeTicker() { MarketSymbol = book[PRODUCTID].ToString(), @@ -224,7 +224,7 @@ protected override async Task OnGetTickerAsync(string marketSymb QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant(), Timestamp = DateTime.UtcNow } - }; + }; } protected override async Task OnGetOrderBookAsync(string marketSymbol, int maxCount = 50) @@ -267,8 +267,8 @@ protected override async Task> OnGetCandlesAsync(strin if ((RangeEnd - RangeStart).TotalSeconds / periodSeconds > 300) RangeStart = RangeEnd.AddSeconds(-(periodSeconds * 300)); List candles = new List(); - while (true) - { + while (true) + { JToken token = await MakeJsonRequestAsync(string.Format("/products/{0}/candles?start={1}&end={2}&granularity={3}", marketSymbol, ((DateTimeOffset)RangeStart).ToUnixTimeSeconds(), ((DateTimeOffset)RangeEnd).ToUnixTimeSeconds(), granularity)); foreach (JToken candle in token["candles"]) candles.Add(this.ParseCandle(candle, marketSymbol, periodSeconds, "open", "high", "low", "close", "start", TimestampType.UnixSeconds, "volume")); if (RangeStart > startDate) @@ -278,7 +278,7 @@ protected override async Task> OnGetCandlesAsync(strin RangeEnd = RangeEnd.AddSeconds(-(periodSeconds * 300)); } else break; - } + } return candles.Where(c => c.Timestamp >= startDate).OrderBy(c => c.Timestamp); } @@ -301,7 +301,7 @@ protected override async Task> OnGetFeesAsync() #region AccountSpecificEndpoints - // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call. + // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call. protected override async Task OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false) { if (Accounts == null) await GetAmounts(true); // Populate Accounts Cache @@ -323,13 +323,13 @@ protected override async Task> OnGetAmountsAvailable return await GetAmounts(true); } - // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call. + // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call. protected override async Task> OnGetWithdrawHistoryAsync(string currency) { return await GetTx(true, currency); } - // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call. + // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call. protected override async Task> OnGetDepositHistoryAsync(string currency) { return await GetTx(false, currency); @@ -344,7 +344,7 @@ protected override async Task> OnGetOpenOrderDe string uri = string.IsNullOrEmpty(marketSymbol) ? "/orders/historical/batch?order_status=OPEN" : $"/orders/historical/batch?product_id={marketSymbol}&order_status=OPEN"; // Parameter order is critical JToken token = await MakeJsonRequestAsync(uri); while(true) - { + { foreach (JToken order in token[ORDERS]) if (order[TYPE].ToStringInvariant().Equals(ADVFILL)) orders.Add(ParseOrder(order)); if (string.IsNullOrEmpty(cursorNext)) break; token = await MakeJsonRequestAsync(uri + "&cursor=" + cursorNext); @@ -360,7 +360,7 @@ protected override async Task> OnGetCompletedOr string uri = string.IsNullOrEmpty(marketSymbol) ? "/orders/historical/batch?order_status=FILLED" : $"/orders/historical/batch?product_id={marketSymbol}&order_status=OPEN"; // Parameter order is critical JToken token = await MakeJsonRequestAsync(uri); while(true) - { + { foreach (JToken order in token[ORDERS]) orders.Add(ParseOrder(order)); if (string.IsNullOrEmpty(cursorNext)) break; token = await MakeJsonRequestAsync(uri + "&cursor=" + cursorNext); @@ -405,12 +405,12 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { {"base_size", order.Amount.ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, - {"end_time", order.ExtraParameters["gtd_timestamp"] }, + {"end_time", order.ExtraParameters["gtd_timestamp"] }, {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", false) } }); } else - { + { orderConfig.Add("limit_limit_gtc", new Dictionary() { {"base_size", order.Amount.ToStringInvariant() }, @@ -454,10 +454,22 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd // The Post doesn't return with any status, just a new OrderId. To get the Order Details we have to reQuery. return await OnGetOrderDetailsAsync(result[ORDERID].ToStringInvariant()); } - catch (Exception ex) // All fails come back with an exception. + catch (Exception ex) // All fails come back with an exception. { + Logger.Error(ex, "Failed to place coinbase error"); var token = JToken.Parse(ex.Message); - return new ExchangeOrderResult(){ Result = ExchangeAPIOrderResult.Rejected, ClientOrderId = order.ClientOrderId, ResultCode = token["error_response"]["error"].ToStringInvariant() }; + return new ExchangeOrderResult(){ + Result = ExchangeAPIOrderResult.Rejected, + IsBuy = payload["side"].ToStringInvariant().Equals(BUY), + MarketSymbol = payload["product_id"].ToStringInvariant(), + ClientOrderId = order.ClientOrderId, + ResultCode = $"{token["error_response"]["error"].ToStringInvariant()} - {token["error_response"]["preview_failure_reason"].ToStringInvariant()}", + AmountFilled = 0, + Amount = order.Amount, + AveragePrice = 0, + Fees = 0, + FeesCurrency = "USDT" + }; } } @@ -509,8 +521,8 @@ protected override Task OnGetDeltaOrderBookWebSocketAsync(Action= maxCount && bidCount >=maxCount) break; } callback?.Invoke(book); - } - return Task.CompletedTask; + } + return Task.CompletedTask; }, async (_socket) => { string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant(); @@ -551,7 +563,7 @@ protected override async Task OnGetTickersWebSocketAsync(Action(), - Timestamp = timestamp + Timestamp = timestamp } } )); } @@ -630,7 +642,7 @@ private async Task> GetAmounts(bool AvailableOnly) } if (string.IsNullOrEmpty(cursorNext)) break; token = await MakeJsonRequestAsync("/accounts?starting_after=" + cursorNext); - } + } pagination = PaginationType.None; return amounts; } @@ -643,12 +655,12 @@ private async Task> GetAmounts(bool AvailableOnly) /// private async Task> GetTx(bool Withdrawals, string currency) { - if (Accounts == null) await GetAmounts(true); + if (Accounts == null) await GetAmounts(true); pagination = PaginationType.V2; List transfers = new List(); JToken tokens = await MakeJsonRequestAsync($"accounts/{Accounts[currency]}/transactions", BaseUrlV2); while(true) - { + { foreach (JToken token in tokens) { // A "send" to Coinbase is when someone "sent" you coin - or a receive to the rest of the world @@ -658,7 +670,7 @@ private async Task> GetTx(bool Withdrawals, string cur } if (string.IsNullOrEmpty(cursorNext)) break; tokens = await MakeJsonRequestAsync($"accounts/{Accounts[currency]}/transactions?starting_after={cursorNext}", BaseUrlV2); - } + } pagination = PaginationType.None; return transfers; } @@ -672,7 +684,7 @@ private ExchangeTransaction ParseTransaction(JToken token) { // The Coin Address/TxFee isn't available but can be retrieved using the Network Hash/BlockChainId return new ExchangeTransaction() - { + { PaymentId = token["id"].ToStringInvariant(), // Not sure how this is used elsewhere but here it is the Coinbase TransactionID BlockchainTxId = token["network"]["hash"].ToStringInvariant(), Currency = token[AMOUNT][CURRENCY].ToStringInvariant(), @@ -680,9 +692,9 @@ private ExchangeTransaction ParseTransaction(JToken token) Timestamp = token["created_at"].ToObject(), Status = token[STATUS].ToStringInvariant() == "completed" ? TransactionStatus.Complete : TransactionStatus.Unknown, Notes = token["description"].ToStringInvariant() - // Address - // AddressTag - // TxFee + // Address + // AddressTag + // TxFee }; } diff --git a/src/ExchangeSharp/ExchangeSharp.csproj b/src/ExchangeSharp/ExchangeSharp.csproj index b056ea89..9e089990 100644 --- a/src/ExchangeSharp/ExchangeSharp.csproj +++ b/src/ExchangeSharp/ExchangeSharp.csproj @@ -8,7 +8,7 @@ 8 DigitalRuby.ExchangeSharp ExchangeSharp - C# API for cryptocurrency exchanges - 1.0.4 + 1.1.0 jjxtra ExchangeSharp is a C# API for working with various cryptocurrency exchanges. Web sockets are also supported for some exchanges. Supported exchanges: Binance BitMEX Bitfinex Bithumb Bitstamp Bittrex BL3P Bleutrade BTSE Cryptopia Coinbase(GDAX) Digifinex Gemini Gitbtc Huobi Kraken Kucoin Livecoin NDAX OKCoin OKEx Poloniex TuxExchange Yobit ZBcom. Pull requests welcome. From 1608eb9527c8b3e028ce61f5a91c87c94dbd4bf5 Mon Sep 17 00:00:00 2001 From: Matt O'Keefe Date: Tue, 23 Jan 2024 13:53:39 -0600 Subject: [PATCH 2/3] Please round if requested --- .../API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 82170730..398e14bf 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -396,6 +396,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd payload["side"] = order.IsBuy ? BUY : "SELL"; Dictionary orderConfig = new Dictionary(); + var amount = order.ShouldRoundAmount ? order.RoundAmount().ToStringInvariant() : order.Amount.ToStringInvariant(); switch (order.OrderType) { case OrderType.Limit: @@ -403,7 +404,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("limit_limit_gtd", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"base_size", amount }, {"limit_price", order.Price.ToStringInvariant() }, {"end_time", order.ExtraParameters["gtd_timestamp"] }, {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", false) } @@ -413,7 +414,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("limit_limit_gtc", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"base_size", amount }, {"limit_price", order.Price.ToStringInvariant() }, {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") } }); @@ -424,7 +425,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("stop_limit_stop_limit_gtd", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"base_size", amount }, {"limit_price", order.Price.ToStringInvariant() }, {"stop_price", order.StopPrice.ToStringInvariant() }, {"end_time", order.ExtraParameters["gtd_timestamp"] }, @@ -434,15 +435,15 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("stop_limit_stop_limit_gtc", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"base_size", amount }, {"limit_price", order.Price.ToStringInvariant() }, {"stop_price", order.StopPrice.ToStringInvariant() }, }); } break; case OrderType.Market: - if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary() { { "quote_size", order.Amount.ToStringInvariant() }}); - else orderConfig.Add("market_market_ioc", new Dictionary() { { "base_size", order.Amount.ToStringInvariant() }}); + if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary() { { "quote_size", amount }}); + else orderConfig.Add("market_market_ioc", new Dictionary() { { "base_size", amount }}); break; } From 32b352910fd4e8b8da149f01bb81cc785d3e18a1 Mon Sep 17 00:00:00 2001 From: Matt O'Keefe Date: Tue, 23 Jan 2024 13:56:01 -0600 Subject: [PATCH 3/3] Helper already checks flag --- .../API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 398e14bf..b9f55f5e 100644 --- a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs +++ b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs @@ -396,7 +396,6 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd payload["side"] = order.IsBuy ? BUY : "SELL"; Dictionary orderConfig = new Dictionary(); - var amount = order.ShouldRoundAmount ? order.RoundAmount().ToStringInvariant() : order.Amount.ToStringInvariant(); switch (order.OrderType) { case OrderType.Limit: @@ -404,7 +403,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("limit_limit_gtd", new Dictionary() { - {"base_size", amount }, + {"base_size", order.RoundAmount().ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, {"end_time", order.ExtraParameters["gtd_timestamp"] }, {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", false) } @@ -414,7 +413,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("limit_limit_gtc", new Dictionary() { - {"base_size", amount }, + {"base_size", order.RoundAmount().ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") } }); @@ -425,7 +424,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("stop_limit_stop_limit_gtd", new Dictionary() { - {"base_size", amount }, + {"base_size", order.RoundAmount().ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, {"stop_price", order.StopPrice.ToStringInvariant() }, {"end_time", order.ExtraParameters["gtd_timestamp"] }, @@ -435,15 +434,15 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("stop_limit_stop_limit_gtc", new Dictionary() { - {"base_size", amount }, + {"base_size", order.RoundAmount().ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, {"stop_price", order.StopPrice.ToStringInvariant() }, }); } break; case OrderType.Market: - if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary() { { "quote_size", amount }}); - else orderConfig.Add("market_market_ioc", new Dictionary() { { "base_size", amount }}); + if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary() { { "quote_size", order.RoundAmount().ToStringInvariant() }}); + else orderConfig.Add("market_market_ioc", new Dictionary() { { "base_size", order.RoundAmount().ToStringInvariant() }}); break; } @@ -466,7 +465,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd ClientOrderId = order.ClientOrderId, ResultCode = $"{token["error_response"]["error"].ToStringInvariant()} - {token["error_response"]["preview_failure_reason"].ToStringInvariant()}", AmountFilled = 0, - Amount = order.Amount, + Amount = order.RoundAmount(), AveragePrice = 0, Fees = 0, FeesCurrency = "USDT"