@@ -29,10 +29,10 @@ public sealed partial class ExchangeCoinbaseAPI : ExchangeAPI
29
29
public override string BaseUrl { get ; set ; } = "https://api.coinbase.com/api/v3/brokerage" ;
30
30
private readonly string BaseUrlV2 = "https://api.coinbase.com/v2" ; // For Wallet Support
31
31
public override string BaseUrlWebSocket { get ; set ; } = "wss://advanced-trade-ws.coinbase.com" ;
32
-
32
+
33
33
private enum PaginationType { None , V2 , V3 }
34
34
private PaginationType pagination = PaginationType . None ;
35
- private string cursorNext ;
35
+ private string cursorNext ;
36
36
37
37
private Dictionary < string , string > Accounts = null ; // Cached Account IDs
38
38
@@ -62,7 +62,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
62
62
JToken token = JsonConvert . DeserializeObject < JToken > ( ( string ) response ) ;
63
63
if ( token == null ) return ;
64
64
switch ( pagination )
65
- {
65
+ {
66
66
case PaginationType . V2 : cursorNext = token [ "pagination" ] ? [ "next_starting_after" ] ? . ToStringInvariant ( ) ; break ;
67
67
case PaginationType . V3 : cursorNext = token [ CURSOR ] ? . ToStringInvariant ( ) ; break ;
68
68
}
@@ -77,7 +77,7 @@ private void ProcessResponse(IAPIRequestMaker maker, RequestMakerState state, ob
77
77
/// <param name="payload"></param>
78
78
/// <returns></returns>
79
79
protected override bool CanMakeAuthenticatedRequest ( IReadOnlyDictionary < string , object > payload )
80
- {
80
+ {
81
81
return ( PrivateApiKey != null && PublicApiKey != null ) ;
82
82
}
83
83
@@ -90,7 +90,7 @@ protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dicti
90
90
91
91
// V2 wants PathAndQuery, V3 wants LocalPath for the sig (I guess they wanted to shave a nano-second or two - silly)
92
92
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 ( ) ) ;
94
94
95
95
request . AddHeader ( "CB-ACCESS-KEY" , PublicApiKey . ToUnsecureString ( ) ) ;
96
96
request . AddHeader ( "CB-ACCESS-SIGN" , signature ) ;
@@ -141,7 +141,7 @@ protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketS
141
141
142
142
protected override async Task < IEnumerable < string > > OnGetMarketSymbolsAsync ( )
143
143
{
144
- return ( await GetMarketSymbolsMetadataAsync ( ) ) . Select ( market => market . MarketSymbol ) ;
144
+ return ( await GetMarketSymbolsMetadataAsync ( ) ) . Select ( market => market . MarketSymbol ) ;
145
145
}
146
146
147
147
protected override async Task < IReadOnlyDictionary < string , ExchangeCurrency > > OnGetCurrenciesAsync ( )
@@ -176,7 +176,7 @@ protected override async Task<IReadOnlyDictionary<string, ExchangeCurrency>> OnG
176
176
currencies [ currency . Name ] = currency ;
177
177
}
178
178
}
179
- return currencies ;
179
+ return currencies ;
180
180
}
181
181
182
182
protected override async Task < IEnumerable < KeyValuePair < string , ExchangeTicker > > > OnGetTickersAsync ( )
@@ -187,7 +187,7 @@ protected override async Task<IEnumerable<KeyValuePair<string, ExchangeTicker>>>
187
187
foreach ( JToken book in books [ PRICEBOOKS ] )
188
188
{
189
189
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
191
191
tickers . Add ( new KeyValuePair < string , ExchangeTicker > ( book [ PRODUCTID ] . ToString ( ) , new ExchangeTicker ( )
192
192
{
193
193
MarketSymbol = book [ PRODUCTID ] . ToString ( ) ,
@@ -224,7 +224,7 @@ protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymb
224
224
QuoteCurrencyVolume = book [ ASKS ] [ 0 ] [ SIZE ] . ConvertInvariant < decimal > ( ) ,
225
225
Timestamp = DateTime . UtcNow
226
226
}
227
- } ;
227
+ } ;
228
228
}
229
229
230
230
protected override async Task < ExchangeOrderBook > OnGetOrderBookAsync ( string marketSymbol , int maxCount = 50 )
@@ -267,8 +267,8 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
267
267
if ( ( RangeEnd - RangeStart ) . TotalSeconds / periodSeconds > 300 ) RangeStart = RangeEnd . AddSeconds ( - ( periodSeconds * 300 ) ) ;
268
268
269
269
List < MarketCandle > candles = new List < MarketCandle > ( ) ;
270
- while ( true )
271
- {
270
+ while ( true )
271
+ {
272
272
JToken token = await MakeJsonRequestAsync < JToken > ( string . Format ( "/products/{0}/candles?start={1}&end={2}&granularity={3}" , marketSymbol , ( ( DateTimeOffset ) RangeStart ) . ToUnixTimeSeconds ( ) , ( ( DateTimeOffset ) RangeEnd ) . ToUnixTimeSeconds ( ) , granularity ) ) ;
273
273
foreach ( JToken candle in token [ "candles" ] ) candles . Add ( this . ParseCandle ( candle , marketSymbol , periodSeconds , "open" , "high" , "low" , "close" , "start" , TimestampType . UnixSeconds , "volume" ) ) ;
274
274
if ( RangeStart > startDate )
@@ -278,7 +278,7 @@ protected override async Task<IEnumerable<MarketCandle>> OnGetCandlesAsync(strin
278
278
RangeEnd = RangeEnd . AddSeconds ( - ( periodSeconds * 300 ) ) ;
279
279
}
280
280
else break ;
281
- }
281
+ }
282
282
return candles . Where ( c => c . Timestamp >= startDate ) . OrderBy ( c => c . Timestamp ) ;
283
283
}
284
284
@@ -301,7 +301,7 @@ protected override async Task<Dictionary<string, decimal>> OnGetFeesAsync()
301
301
302
302
#region AccountSpecificEndpoints
303
303
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.
305
305
protected override async Task < ExchangeDepositDetails > OnGetDepositAddressAsync ( string symbol , bool forceRegenerate = false )
306
306
{
307
307
if ( Accounts == null ) await GetAmounts ( true ) ; // Populate Accounts Cache
@@ -323,13 +323,13 @@ protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailable
323
323
return await GetAmounts ( true ) ;
324
324
}
325
325
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.
327
327
protected override async Task < IEnumerable < ExchangeTransaction > > OnGetWithdrawHistoryAsync ( string currency )
328
328
{
329
329
return await GetTx ( true , currency ) ;
330
330
}
331
331
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.
333
333
protected override async Task < IEnumerable < ExchangeTransaction > > OnGetDepositHistoryAsync ( string currency )
334
334
{
335
335
return await GetTx ( false , currency ) ;
@@ -344,7 +344,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDe
344
344
string uri = string . IsNullOrEmpty ( marketSymbol ) ? "/orders/historical/batch?order_status=OPEN" : $ "/orders/historical/batch?product_id={ marketSymbol } &order_status=OPEN"; // Parameter order is critical
345
345
JToken token = await MakeJsonRequestAsync < JToken > ( uri ) ;
346
346
while ( true )
347
- {
347
+ {
348
348
foreach ( JToken order in token [ ORDERS ] ) if ( order [ TYPE ] . ToStringInvariant ( ) . Equals ( ADVFILL ) ) orders . Add ( ParseOrder ( order ) ) ;
349
349
if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
350
350
token = await MakeJsonRequestAsync < JToken > ( uri + "&cursor=" + cursorNext ) ;
@@ -360,7 +360,7 @@ protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetCompletedOr
360
360
string uri = string . IsNullOrEmpty ( marketSymbol ) ? "/orders/historical/batch?order_status=FILLED" : $ "/orders/historical/batch?product_id={ marketSymbol } &order_status=OPEN"; // Parameter order is critical
361
361
JToken token = await MakeJsonRequestAsync < JToken > ( uri ) ;
362
362
while ( true )
363
- {
363
+ {
364
364
foreach ( JToken order in token [ ORDERS ] ) orders . Add ( ParseOrder ( order ) ) ;
365
365
if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
366
366
token = await MakeJsonRequestAsync < JToken > ( uri + "&cursor=" + cursorNext ) ;
@@ -403,17 +403,17 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
403
403
{
404
404
orderConfig . Add ( "limit_limit_gtd" , new Dictionary < string , object > ( )
405
405
{
406
- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
406
+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
407
407
{ "limit_price" , order . Price . ToStringInvariant ( ) } ,
408
- { "end_time" , order . ExtraParameters [ "gtd_timestamp" ] } ,
408
+ { "end_time" , order . ExtraParameters [ "gtd_timestamp" ] } ,
409
409
{ "post_only" , order . ExtraParameters . TryGetValueOrDefault ( "post_only" , false ) }
410
410
} ) ;
411
411
}
412
412
else
413
- {
413
+ {
414
414
orderConfig . Add ( "limit_limit_gtc" , new Dictionary < string , object > ( )
415
415
{
416
- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
416
+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
417
417
{ "limit_price" , order . Price . ToStringInvariant ( ) } ,
418
418
{ "post_only" , order . ExtraParameters . TryGetValueOrDefault ( "post_only" , "false" ) }
419
419
} ) ;
@@ -424,7 +424,7 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
424
424
{
425
425
orderConfig . Add ( "stop_limit_stop_limit_gtd" , new Dictionary < string , object > ( )
426
426
{
427
- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
427
+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
428
428
{ "limit_price" , order . Price . ToStringInvariant ( ) } ,
429
429
{ "stop_price" , order . StopPrice . ToStringInvariant ( ) } ,
430
430
{ "end_time" , order . ExtraParameters [ "gtd_timestamp" ] } ,
@@ -434,15 +434,15 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
434
434
{
435
435
orderConfig . Add ( "stop_limit_stop_limit_gtc" , new Dictionary < string , object > ( )
436
436
{
437
- { "base_size" , order . Amount . ToStringInvariant ( ) } ,
437
+ { "base_size" , order . RoundAmount ( ) . ToStringInvariant ( ) } ,
438
438
{ "limit_price" , order . Price . ToStringInvariant ( ) } ,
439
439
{ "stop_price" , order . StopPrice . ToStringInvariant ( ) } ,
440
440
} ) ;
441
441
}
442
442
break ;
443
443
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 ( ) } } ) ;
446
446
break ;
447
447
}
448
448
@@ -454,10 +454,22 @@ protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrd
454
454
// The Post doesn't return with any status, just a new OrderId. To get the Order Details we have to reQuery.
455
455
return await OnGetOrderDetailsAsync ( result [ ORDERID ] . ToStringInvariant ( ) ) ;
456
456
}
457
- catch ( Exception ex ) // All fails come back with an exception.
457
+ catch ( Exception ex ) // All fails come back with an exception.
458
458
{
459
+ Logger . Error ( ex , "Failed to place coinbase error" ) ;
459
460
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
+ } ;
461
473
}
462
474
}
463
475
@@ -509,8 +521,8 @@ protected override Task<IWebSocket> OnGetDeltaOrderBookWebSocketAsync(Action<Exc
509
521
if ( askCount >= maxCount && bidCount >= maxCount ) break ;
510
522
}
511
523
callback ? . Invoke ( book ) ;
512
- }
513
- return Task . CompletedTask ;
524
+ }
525
+ return Task . CompletedTask ;
514
526
} , async ( _socket ) =>
515
527
{
516
528
string timestamp = DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) . ToStringInvariant ( ) ;
@@ -551,7 +563,7 @@ protected override async Task<IWebSocket> OnGetTickersWebSocketAsync(Action<IRea
551
563
BaseCurrency = split [ 0 ] ,
552
564
QuoteCurrency = split [ 1 ] ,
553
565
BaseCurrencyVolume = token [ "volume_24_h" ] . ConvertInvariant < decimal > ( ) ,
554
- Timestamp = timestamp
566
+ Timestamp = timestamp
555
567
}
556
568
} ) ) ;
557
569
}
@@ -630,7 +642,7 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
630
642
}
631
643
if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
632
644
token = await MakeJsonRequestAsync < JToken > ( "/accounts?starting_after=" + cursorNext ) ;
633
- }
645
+ }
634
646
pagination = PaginationType . None ;
635
647
return amounts ;
636
648
}
@@ -643,12 +655,12 @@ private async Task<Dictionary<string, decimal>> GetAmounts(bool AvailableOnly)
643
655
/// <returns></returns>
644
656
private async Task < List < ExchangeTransaction > > GetTx ( bool Withdrawals , string currency )
645
657
{
646
- if ( Accounts == null ) await GetAmounts ( true ) ;
658
+ if ( Accounts == null ) await GetAmounts ( true ) ;
647
659
pagination = PaginationType . V2 ;
648
660
List < ExchangeTransaction > transfers = new List < ExchangeTransaction > ( ) ;
649
661
JToken tokens = await MakeJsonRequestAsync < JToken > ( $ "accounts/{ Accounts [ currency ] } /transactions", BaseUrlV2 ) ;
650
662
while ( true )
651
- {
663
+ {
652
664
foreach ( JToken token in tokens )
653
665
{
654
666
// 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
658
670
}
659
671
if ( string . IsNullOrEmpty ( cursorNext ) ) break ;
660
672
tokens = await MakeJsonRequestAsync < JToken > ( $ "accounts/{ Accounts [ currency ] } /transactions?starting_after={ cursorNext } ", BaseUrlV2 ) ;
661
- }
673
+ }
662
674
pagination = PaginationType . None ;
663
675
return transfers ;
664
676
}
@@ -672,17 +684,17 @@ private ExchangeTransaction ParseTransaction(JToken token)
672
684
{
673
685
// The Coin Address/TxFee isn't available but can be retrieved using the Network Hash/BlockChainId
674
686
return new ExchangeTransaction ( )
675
- {
687
+ {
676
688
PaymentId = token [ "id" ] . ToStringInvariant ( ) , // Not sure how this is used elsewhere but here it is the Coinbase TransactionID
677
689
BlockchainTxId = token [ "network" ] [ "hash" ] . ToStringInvariant ( ) ,
678
690
Currency = token [ AMOUNT ] [ CURRENCY ] . ToStringInvariant ( ) ,
679
691
Amount = token [ AMOUNT ] [ AMOUNT ] . ConvertInvariant < decimal > ( ) ,
680
692
Timestamp = token [ "created_at" ] . ToObject < DateTime > ( ) ,
681
693
Status = token [ STATUS ] . ToStringInvariant ( ) == "completed" ? TransactionStatus . Complete : TransactionStatus . Unknown ,
682
694
Notes = token [ "description" ] . ToStringInvariant ( )
683
- // Address
684
- // AddressTag
685
- // TxFee
695
+ // Address
696
+ // AddressTag
697
+ // TxFee
686
698
} ;
687
699
}
688
700
0 commit comments