Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Adding better errors for coinbase order errors #826

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
89 changes: 51 additions & 38 deletions src/ExchangeSharp/API/Exchanges/Coinbase/ExchangeCoinbaseAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> Accounts = null; // Cached Account IDs

Expand Down Expand Up @@ -62,7 +62,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
JToken token = JsonConvert.DeserializeObject<JToken>((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;
}
Expand All @@ -77,7 +77,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
/// <param name="payload"></param>
/// <returns></returns>
protected override bool CanMakeAuthenticatedRequest(IReadOnlyDictionary<string, object> payload)
{
{
return (PrivateApiKey != null && PublicApiKey != null);
}

Expand All @@ -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);
Expand Down Expand Up @@ -141,7 +141,7 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS

protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
{
return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol);
return (await GetMarketSymbolsMetadataAsync()).Select(market => market.MarketSymbol);
}

protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnGetCurrenciesAsync()
Expand Down Expand Up @@ -176,7 +176,7 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
currencies[currency.Name] = currency;
}
}
return currencies;
return currencies;
}

protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>> OnGetTickersAsync()
Expand All @@ -187,7 +187,7 @@ protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>>
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<string, ExchangeTicker>(book[PRODUCTID].ToString(), new ExchangeTicker()
{
MarketSymbol = book[PRODUCTID].ToString(),
Expand Down Expand Up @@ -224,7 +224,7 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
QuoteCurrencyVolume = book[ASKS][0][SIZE].ConvertInvariant<decimal>(),
Timestamp = DateTime.UtcNow
}
};
};
}

protected override async Task<ExchangeOrderBook> OnGetOrderBookAsync(string marketSymbol, int maxCount = 50)
Expand Down Expand Up @@ -267,8 +267,8 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
if ((RangeEnd - RangeStart).TotalSeconds / periodSeconds > 300) RangeStart = RangeEnd.AddSeconds(-(periodSeconds * 300));

List<MarketCandle> candles = new List<MarketCandle>();
while (true)
{
while (true)
{
JToken token = await MakeJsonRequestAsync<JToken>(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)
Expand All @@ -278,7 +278,7 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
RangeEnd = RangeEnd.AddSeconds(-(periodSeconds * 300));
}
else break;
}
}
return candles.Where(c => c.Timestamp >= startDate).OrderBy(c => c.Timestamp);
}

Expand All @@ -301,7 +301,7 @@ protected override async Task<Dictionary<string, decimal>> 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<ExchangeDepositDetails> OnGetDepositAddressAsync(string symbol, bool forceRegenerate = false)
{
if (Accounts == null) await GetAmounts(true); // Populate Accounts Cache
Expand All @@ -323,13 +323,13 @@ protected override async Task<Dictionary<string, decimal>> 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<IEnumerable<ExchangeTransaction>> 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<IEnumerable<ExchangeTransaction>> OnGetDepositHistoryAsync(string currency)
{
return await GetTx(false, currency);
Expand All @@ -344,7 +344,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> 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<JToken>(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<JToken>(uri + "&cursor=" + cursorNext);
Expand All @@ -360,7 +360,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> 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<JToken>(uri);
while(true)
{
{
foreach (JToken order in token[ORDERS]) orders.Add(ParseOrder(order));
if (string.IsNullOrEmpty(cursorNext)) break;
token = await MakeJsonRequestAsync<JToken>(uri + "&cursor=" + cursorNext);
Expand Down Expand Up @@ -396,24 +396,25 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
payload["side"] = order.IsBuy ? BUY : "SELL";

Dictionary<string, object> orderConfig = new Dictionary<string, object>();
var amount = order.ShouldRoundAmount ? order.RoundAmount().ToStringInvariant() : order.Amount.ToStringInvariant();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We check the ShouldRound and then RoundAmount if needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check happens already in order.RoundAmount so i moved back to using the method instead of this variable check

switch (order.OrderType)
{
case OrderType.Limit:
if (order.ExtraParameters.ContainsKey("gtd_timestamp"))
{
orderConfig.Add("limit_limit_gtd", new Dictionary<string, object>()
{
{"base_size", order.Amount.ToStringInvariant() },
{"base_size", amount },
{"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<string, object>()
{
{"base_size", order.Amount.ToStringInvariant() },
{"base_size", amount },
{"limit_price", order.Price.ToStringInvariant() },
{"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
});
Expand All @@ -424,7 +425,7 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
{
orderConfig.Add("stop_limit_stop_limit_gtd", new Dictionary<string, object>()
{
{"base_size", order.Amount.ToStringInvariant() },
{"base_size", amount },
{"limit_price", order.Price.ToStringInvariant() },
{"stop_price", order.StopPrice.ToStringInvariant() },
{"end_time", order.ExtraParameters["gtd_timestamp"] },
Expand All @@ -434,15 +435,15 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
{
orderConfig.Add("stop_limit_stop_limit_gtc", new Dictionary<string, object>()
{
{"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<string, object>() { { "quote_size", order.Amount.ToStringInvariant() }});
else orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "base_size", order.Amount.ToStringInvariant() }});
if (order.IsBuy) orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "quote_size", amount }});
else orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "base_size", amount }});
break;
}

Expand All @@ -454,10 +455,22 @@ protected override async Task<ExchangeOrderResult> 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");
jjxtra marked this conversation as resolved.
Show resolved Hide resolved
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"
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

include order details since the error handler uses .ToString() on the ExchangeOrderResults and without details it is useless. Also token["error_response"]["error"] seems insufficient in many cases so including token["error_response"]["preview_failure_reason"] as well

}
}

Expand Down Expand Up @@ -509,8 +522,8 @@ protected override Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<Exc
if (askCount >= maxCount && bidCount >=maxCount) break;
}
callback?.Invoke(book);
}
return Task.CompletedTask;
}
return Task.CompletedTask;
}, async (_socket) =>
{
string timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToStringInvariant();
Expand Down Expand Up @@ -551,7 +564,7 @@ protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IRea
BaseCurrency = split[0],
QuoteCurrency = split[1],
BaseCurrencyVolume = token["volume_24_h"].ConvertInvariant<decimal>(),
Timestamp = timestamp
Timestamp = timestamp
}
} ));
}
Expand Down Expand Up @@ -630,7 +643,7 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
}
if (string.IsNullOrEmpty(cursorNext)) break;
token = await MakeJsonRequestAsync<JToken>("/accounts?starting_after=" + cursorNext);
}
}
pagination = PaginationType.None;
return amounts;
}
Expand All @@ -643,12 +656,12 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
/// <returns></returns>
private async Task<List<ExchangeTransaction>> GetTx(bool Withdrawals, string currency)
{
if (Accounts == null) await GetAmounts(true);
if (Accounts == null) await GetAmounts(true);
pagination = PaginationType.V2;
List<ExchangeTransaction> transfers = new List<ExchangeTransaction>();
JToken tokens = await MakeJsonRequestAsync<JToken>($"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
Expand All @@ -658,7 +671,7 @@ private async Task<List<ExchangeTransaction>> GetTx(bool Withdrawals, string cur
}
if (string.IsNullOrEmpty(cursorNext)) break;
tokens = await MakeJsonRequestAsync<JToken>($"accounts/{Accounts[currency]}/transactions?starting_after={cursorNext}", BaseUrlV2);
}
}
pagination = PaginationType.None;
return transfers;
}
Expand All @@ -672,17 +685,17 @@ 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(),
Amount = token[AMOUNT][AMOUNT].ConvertInvariant<decimal>(),
Timestamp = token["created_at"].ToObject<DateTime>(),
Status = token[STATUS].ToStringInvariant() == "completed" ? TransactionStatus.Complete : TransactionStatus.Unknown,
Notes = token["description"].ToStringInvariant()
// Address
// AddressTag
// TxFee
// Address
// AddressTag
// TxFee
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/ExchangeSharp/ExchangeSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<LangVersion>8</LangVersion>
<PackageId>DigitalRuby.ExchangeSharp</PackageId>
<Title>ExchangeSharp - C# API for cryptocurrency exchanges</Title>
<VersionPrefix>1.0.4</VersionPrefix>
<VersionPrefix>1.1.0</VersionPrefix>
<Authors>jjxtra</Authors>
<Description>ExchangeSharp is a C# API for working with various cryptocurrency exchanges. Web sockets are also supported for some exchanges.</Description>
<Summary>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.</Summary>
Expand Down
Loading