diff --git a/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs b/src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs index 81ba7ac3..b9f55f5e 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); @@ -403,17 +403,17 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("limit_limit_gtd", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"base_size", order.RoundAmount().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() }, + {"base_size", order.RoundAmount().ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, {"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") } }); @@ -424,7 +424,7 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("stop_limit_stop_limit_gtd", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"base_size", order.RoundAmount().ToStringInvariant() }, {"limit_price", order.Price.ToStringInvariant() }, {"stop_price", order.StopPrice.ToStringInvariant() }, {"end_time", order.ExtraParameters["gtd_timestamp"] }, @@ -434,15 +434,15 @@ protected override async Task OnPlaceOrderAsync(ExchangeOrd { orderConfig.Add("stop_limit_stop_limit_gtc", new Dictionary() { - {"base_size", order.Amount.ToStringInvariant() }, + {"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", 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", order.RoundAmount().ToStringInvariant() }}); + else orderConfig.Add("market_market_ioc", new Dictionary() { { "base_size", order.RoundAmount().ToStringInvariant() }}); break; } @@ -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.RoundAmount(), + 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.