Skip to content

Commit

Permalink
chore: Adding better errors for coinbase order errors (#826)
Browse files Browse the repository at this point in the history
* chore: Adding better errors for coinbase order errors

* Please round if requested

* Helper already checks flag
  • Loading branch information
O-Mutt authored Jan 23, 2024
1 parent 33be336 commit 6a21325
Showing 1 changed file with 50 additions and 38 deletions.
88 changes: 50 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 @@ -403,17 +403,17 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
{
orderConfig.Add("limit_limit_gtd", new Dictionary<string, object>()
{
{"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<string, object>()
{
{"base_size", order.Amount.ToStringInvariant() },
{"base_size", order.RoundAmount().ToStringInvariant() },
{"limit_price", order.Price.ToStringInvariant() },
{"post_only", order.ExtraParameters.TryGetValueOrDefault( "post_only", "false") }
});
Expand All @@ -424,7 +424,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", order.RoundAmount().ToStringInvariant() },
{"limit_price", order.Price.ToStringInvariant() },
{"stop_price", order.StopPrice.ToStringInvariant() },
{"end_time", order.ExtraParameters["gtd_timestamp"] },
Expand All @@ -434,15 +434,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", 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<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", order.RoundAmount().ToStringInvariant() }});
else orderConfig.Add("market_market_ioc", new Dictionary<string, object>() { { "base_size", order.RoundAmount().ToStringInvariant() }});
break;
}

Expand All @@ -454,10 +454,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");
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"
};
}
}

Expand Down Expand Up @@ -509,8 +521,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 +563,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 +642,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 +655,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 +670,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 +684,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

0 comments on commit 6a21325

Please sign in to comment.