@@ -29,10 +29,10 @@ public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI
2929 public override string BaseUrl { get ; set ; } = "https://api.coinbase.com/api/v3/brokerage" ;
3030 private readonly string BaseUrlV2 = "https://api.coinbase.com/v2" ; // For Wallet Support
3131 public override string BaseUrlWebSocket { get ; set ; } = "wss://advanced-trade-ws.coinbase.com" ;
32-
32+
3333 private enum PaginationType { None , V2 , V3 }
3434 private PaginationType pagination = PaginationType . None ;
35- private string cursorNext ;
35+ private string cursorNext ;
3636
3737 private Dictionary < string , string > Accounts = null ; // Cached Account IDs
3838
@@ -62,7 +62,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
6262 JToken token = JsonConvert . DeserializeObject < JToken > ( ( string ) response ) ;
6363 if ( token == null ) return ;
6464 switch ( pagination )
65- {
65+ {
6666 case PaginationType . V2 : cursorNext = token [ "pagination" ] ? [ "next_starting_after" ] ? . ToStringInvariant ( ) ; break ;
6767 case PaginationType . V3 : cursorNext = token [ CURSOR ] ? . ToStringInvariant ( ) ; break ;
6868 }
@@ -77,7 +77,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
7777 /// <param name="payload"></param>
7878 /// <returns></returns>
7979 protected override bool CanMakeAuthenticatedRequest ( IReadOnlyDictionary < string , object > payload )
80- {
80+ {
8181 return ( PrivateApiKey != null && PublicApiKey != null ) ;
8282 }
8383
@@ -90,7 +90,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti
9090
9191 // V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
9292 string path = request . RequestUri . AbsoluteUri . StartsWith ( BaseUrlV2 ) ? request . RequestUri . PathAndQuery : request . RequestUri . LocalPath ;
93- string signature = CryptoUtility . SHA256Sign ( timestamp + request . Method . ToUpperInvariant ( ) + path + body , PrivateApiKey . ToUnsecureString ( ) ) ;
93+ string signature = CryptoUtility . SHA256Sign ( timestamp + request . Method . ToUpperInvariant ( ) + path + body , PrivateApiKey . ToUnsecureString ( ) ) ;
9494
9595 request . AddHeader ( "CB-ACCESS-KEY" , PublicApiKey . ToUnsecureString ( ) ) ;
9696 request . AddHeader ( "CB-ACCESS-SIGN" , signature ) ;
@@ -141,7 +141,7 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
141141
142142 protected override async Task < IEnumerable < string > > OnGetMarketSymbolsAsync ( )
143143 {
144- return ( await GetMarketSymbolsMetadataAsync ( ) ) . Select ( market => market . MarketSymbol ) ;
144+ return ( await GetMarketSymbolsMetadataAsync ( ) ) . Select ( market => market . MarketSymbol ) ;
145145 }
146146
147147 protected override async Task < IReadOnlyDictionary < string , ExchangeCurrency > > OnGetCurrenciesAsync ( )
@@ -176,7 +176,7 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
176176 currencies [ currency . Name ] = currency ;
177177 }
178178 }
179- return currencies ;
179+ return currencies ;
180180 }
181181
182182 protected override async Task < IEnumerable < KeyValuePair < string , ExchangeTicker > > > OnGetTickersAsync ( )
@@ -187,7 +187,7 @@ protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>>
187187 foreach ( JToken book in books [ PRICEBOOKS ] )
188188 {
189189 var split = book [ PRODUCTID ] . ToString ( ) . Split ( GlobalMarketSymbolSeparator ) ;
190- // This endpoint does not provide a last or open for the ExchangeTicker
190+ // This endpoint does not provide a last or open for the ExchangeTicker
191191 tickers . Add ( new KeyValuePair < string , ExchangeTicker > ( book [ PRODUCTID ] . ToString ( ) , new ExchangeTicker ( )
192192 {
193193 MarketSymbol = book [ PRODUCTID ] . ToString ( ) ,
@@ -224,7 +224,7 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
224224 QuoteCurrencyVolume = book [ ASKS ] [ 0 ] [ SIZE ] . ConvertInvariant < decimal > ( ) ,
225225 Timestamp = DateTime . UtcNow
226226 }
227- } ;
227+ } ;
228228 }
229229
230230 protected override async Task < ExchangeOrderBook > OnGetOrderBookAsync ( string marketSymbol , int maxCount = 50 )
@@ -267,8 +267,8 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
267267 if ( ( RangeEnd - RangeStart ) . TotalSeconds / periodSeconds > 300 ) RangeStart = RangeEnd . AddSeconds ( - ( periodSeconds * 300 ) ) ;
268268
269269 List < MarketCandle > candles = new List < MarketCandle > ( ) ;
270- while ( true )
271- {
270+ while ( true )
271+ {
272272 JToken token = await MakeJsonRequestAsync < JToken > ( string . Format ( "/products/{0}/candles?start={1}&end={2}&granularity={3}" , marketSymbol , ( ( DateTimeOffset ) RangeStart ) . ToUnixTimeSeconds ( ) , ( ( DateTimeOffset ) RangeEnd ) . ToUnixTimeSeconds ( ) , granularity ) ) ;
273273 foreach ( JToken candle in token [ "candles" ] ) candles . Add ( this . ParseCandle ( candle , marketSymbol , periodSeconds , "open" , "high" , "low" , "close" , "start" , TimestampType . UnixSeconds , "volume" ) ) ;
274274 if ( RangeStart > startDate )
@@ -278,7 +278,7 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
278278 RangeEnd = RangeEnd . AddSeconds ( - ( periodSeconds * 300 ) ) ;
279279 }
280280 else break ;
281- }
281+ }
282282 return candles . Where ( c => c . Timestamp >= startDate ) . OrderBy ( c => c . Timestamp ) ;
283283 }
284284
@@ -301,7 +301,7 @@ protected override async Task<Dictionary<string, decimal>> OnGetFeesAsync()
301301
302302 #region AccountSpecificEndpoints
303303
304- // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
304+ // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
305305 protected override async Task < ExchangeDepositDetails > OnGetDepositAddressAsync ( string symbol , bool forceRegenerate = false )
306306 {
307307 if ( Accounts == null ) await GetAmounts ( true ) ; // Populate Accounts Cache
@@ -323,13 +323,13 @@ protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailable
323323 return await GetAmounts ( true ) ;
324324 }
325325
326- // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
326+ // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
327327 protected override async Task < IEnumerable < ExchangeTransaction > > OnGetWithdrawHistoryAsync ( string currency )
328328 {
329329 return await GetTx ( true , currency ) ;
330330 }
331331
332- // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
332+ // WARNING: Currently V3 doesn't support Coinbase Wallet APIs, so we are reverting to V2 for this call.
333333 protected override async Task < IEnumerable < ExchangeTransaction > > OnGetDepositHistoryAsync ( string currency )
334334 {
335335 return await GetTx ( false , currency ) ;
@@ -344,7 +344,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDe
344344 string uri = string . IsNullOrEmpty ( marketSymbol ) ? "/orders/historical/batch?order_status=OPEN" : $ "/orders/historical/batch?product_id={ marketSymbol } &order_status=OPEN"; // Parameter order is critical
345345 JToken token = await MakeJsonRequestAsync < JToken > ( uri ) ;
346346 while ( true )
347- {
347+ {
348348 foreach ( JToken order in token [ ORDERS ] ) if ( order [ TYPE ] . ToStringInvariant ( ) . Equals ( ADVFILL ) ) orders . Add ( ParseOrder ( order ) ) ;
349349 if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
350350 token = await MakeJsonRequestAsync < JToken > ( uri + "&cursor=" + cursorNext ) ;
@@ -360,7 +360,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetCompletedOr
360360 string uri = string . IsNullOrEmpty ( marketSymbol ) ? "/orders/historical/batch?order_status=FILLED" : $ "/orders/historical/batch?product_id={ marketSymbol } &order_status=OPEN"; // Parameter order is critical
361361 JToken token = await MakeJsonRequestAsync < JToken > ( uri ) ;
362362 while ( true )
363- {
363+ {
364364 foreach ( JToken order in token [ ORDERS ] ) orders . Add ( ParseOrder ( order ) ) ;
365365 if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
366366 token = await MakeJsonRequestAsync < JToken > ( uri + "&cursor=" + cursorNext ) ;
@@ -403,17 +403,17 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
403403 {
404404 orderConfig . Add ( "limit_limit_gtd" , new Dictionary < string , object > ( )
405405 {
406- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
406+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
407407 { "limit_price" , order . Price . ToStringInvariant ( ) } ,
408- { "end_time" , order . ExtraParameters [ "gtd_timestamp" ] } ,
408+ { "end_time" , order . ExtraParameters [ "gtd_timestamp" ] } ,
409409 { "post_only" , order . ExtraParameters . TryGetValueOrDefault ( "post_only" , false ) }
410410 } ) ;
411411 }
412412 else
413- {
413+ {
414414 orderConfig . Add ( "limit_limit_gtc" , new Dictionary < string , object > ( )
415415 {
416- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
416+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
417417 { "limit_price" , order . Price . ToStringInvariant ( ) } ,
418418 { "post_only" , order . ExtraParameters . TryGetValueOrDefault ( "post_only" , "false" ) }
419419 } ) ;
@@ -424,7 +424,7 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
424424 {
425425 orderConfig . Add ( "stop_limit_stop_limit_gtd" , new Dictionary < string , object > ( )
426426 {
427- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
427+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
428428 { "limit_price" , order . Price . ToStringInvariant ( ) } ,
429429 { "stop_price" , order . StopPrice . ToStringInvariant ( ) } ,
430430 { "end_time" , order . ExtraParameters [ "gtd_timestamp" ] } ,
@@ -434,15 +434,15 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
434434 {
435435 orderConfig . Add ( "stop_limit_stop_limit_gtc" , new Dictionary < string , object > ( )
436436 {
437- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
437+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
438438 { "limit_price" , order . Price . ToStringInvariant ( ) } ,
439439 { "stop_price" , order . StopPrice . ToStringInvariant ( ) } ,
440440 } ) ;
441441 }
442442 break ;
443443 case OrderType . Market :
444- if ( order . IsBuy ) orderConfig . Add ( "market_market_ioc" , new Dictionary < string , object > ( ) { { "quote_size" , order . Amount . ToStringInvariant ( ) } } ) ;
445- else orderConfig . Add ( "market_market_ioc" , new Dictionary < string , object > ( ) { { "base_size" , order . Amount . ToStringInvariant ( ) } } ) ;
444+ if ( order . IsBuy ) orderConfig . Add ( "market_market_ioc" , new Dictionary < string , object > ( ) { { "quote_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } } ) ;
445+ else orderConfig . Add ( "market_market_ioc" , new Dictionary < string , object > ( ) { { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } } ) ;
446446 break ;
447447 }
448448
@@ -454,10 +454,22 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
454454 // The Post doesn't return with any status, just a new OrderId. To get the Order Details we have to reQuery.
455455 return await OnGetOrderDetailsAsync ( result [ ORDERID ] . ToStringInvariant ( ) ) ;
456456 }
457- catch ( Exception ex ) // All fails come back with an exception.
457+ catch ( Exception ex ) // All fails come back with an exception.
458458 {
459+ Logger . Error ( ex , "Failed to place coinbase error" ) ;
459460 var token = JToken . Parse ( ex . Message ) ;
460- return new ExchangeOrderResult ( ) { Result = ExchangeAPIOrderResult . Rejected , ClientOrderId = order . ClientOrderId , ResultCode = token [ "error_response" ] [ "error" ] . ToStringInvariant ( ) } ;
461+ return new ExchangeOrderResult ( ) {
462+ Result = ExchangeAPIOrderResult . Rejected ,
463+ IsBuy = payload [ "side" ] . ToStringInvariant ( ) . Equals ( BUY ) ,
464+ MarketSymbol = payload [ "product_id" ] . ToStringInvariant ( ) ,
465+ ClientOrderId = order . ClientOrderId ,
466+ ResultCode = $ "{ token [ "error_response" ] [ "error" ] . ToStringInvariant ( ) } - { token [ "error_response" ] [ "preview_failure_reason" ] . ToStringInvariant ( ) } ",
467+ AmountFilled = 0 ,
468+ Amount = order . RoundAmount ( ) ,
469+ AveragePrice = 0 ,
470+ Fees = 0 ,
471+ FeesCurrency = "USDT"
472+ } ;
461473 }
462474 }
463475
@@ -509,8 +521,8 @@ protected override Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<Exc
509521 if ( askCount >= maxCount && bidCount >= maxCount ) break ;
510522 }
511523 callback ? . Invoke ( book ) ;
512- }
513- return Task . CompletedTask ;
524+ }
525+ return Task . CompletedTask ;
514526 } , async ( _socket ) =>
515527 {
516528 string timestamp = DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) . ToStringInvariant ( ) ;
@@ -551,7 +563,7 @@ protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IRea
551563 BaseCurrency = split [ 0 ] ,
552564 QuoteCurrency = split [ 1 ] ,
553565 BaseCurrencyVolume = token [ "volume_24_h" ] . ConvertInvariant < decimal > ( ) ,
554- Timestamp = timestamp
566+ Timestamp = timestamp
555567 }
556568 } ) ) ;
557569 }
@@ -630,7 +642,7 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
630642 }
631643 if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
632644 token = await MakeJsonRequestAsync < JToken > ( "/accounts?starting_after=" + cursorNext ) ;
633- }
645+ }
634646 pagination = PaginationType . None ;
635647 return amounts ;
636648 }
@@ -643,12 +655,12 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
643655 /// <returns></returns>
644656 private async Task < List < ExchangeTransaction > > GetTx ( bool Withdrawals , string currency )
645657 {
646- if ( Accounts == null ) await GetAmounts ( true ) ;
658+ if ( Accounts == null ) await GetAmounts ( true ) ;
647659 pagination = PaginationType . V2 ;
648660 List < ExchangeTransaction > transfers = new List < ExchangeTransaction > ( ) ;
649661 JToken tokens = await MakeJsonRequestAsync < JToken > ( $ "accounts/{ Accounts [ currency ] } /transactions", BaseUrlV2 ) ;
650662 while ( true )
651- {
663+ {
652664 foreach ( JToken token in tokens )
653665 {
654666 // 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<List<ExchangeTransaction>> GetTx(bool Withdrawals, string cur
658670 }
659671 if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
660672 tokens = await MakeJsonRequestAsync < JToken > ( $ "accounts/{ Accounts [ currency ] } /transactions?starting_after={ cursorNext } ", BaseUrlV2 ) ;
661- }
673+ }
662674 pagination = PaginationType . None ;
663675 return transfers ;
664676 }
@@ -672,17 +684,17 @@ private ExchangeTransaction ParseTransaction(JToken token)
672684 {
673685 // The Coin Address/TxFee isn't available but can be retrieved using the Network Hash/BlockChainId
674686 return new ExchangeTransaction ( )
675- {
687+ {
676688 PaymentId = token [ "id" ] . ToStringInvariant ( ) , // Not sure how this is used elsewhere but here it is the Coinbase TransactionID
677689 BlockchainTxId = token [ "network" ] [ "hash" ] . ToStringInvariant ( ) ,
678690 Currency = token [ AMOUNT ] [ CURRENCY ] . ToStringInvariant ( ) ,
679691 Amount = token [ AMOUNT ] [ AMOUNT ] . ConvertInvariant < decimal > ( ) ,
680692 Timestamp = token [ "created_at" ] . ToObject < DateTime > ( ) ,
681693 Status = token [ STATUS ] . ToStringInvariant ( ) == "completed" ? TransactionStatus . Complete : TransactionStatus . Unknown ,
682694 Notes = token [ "description" ] . ToStringInvariant ( )
683- // Address
684- // AddressTag
685- // TxFee
695+ // Address
696+ // AddressTag
697+ // TxFee
686698 } ;
687699 }
688700
0 commit comments