Skip to content

Commit 48c5bef

Browse files
authored
Feature/coinmate (DigitalRuby#718)
1 parent 6e327ab commit 48c5bef

File tree

10 files changed

+410
-0
lines changed

10 files changed

+410
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The following cryptocurrency exchanges are supported:
4242
| BTSE | x | x | |
4343
| Bybit | x | x | R | Has public method for Websocket Positions
4444
| Coinbase | x | x | T R U |
45+
| Coinmate | x | x | |
4546
| Digifinex | x | x | R B |
4647
| FTX | x | x | T |
4748
| gate.io | x | x | |
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
using ExchangeSharp.API.Exchanges.Coinmate.Models;
2+
using Newtonsoft.Json.Linq;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Threading.Tasks;
7+
8+
namespace ExchangeSharp
9+
{
10+
public class ExchangeCoinmateAPI : ExchangeAPI
11+
{
12+
public override string BaseUrl { get; set; } = "https://coinmate.io/api";
13+
14+
public ExchangeCoinmateAPI()
15+
{
16+
RequestContentType = "application/x-www-form-urlencoded";
17+
MarketSymbolSeparator = "_";
18+
NonceStyle = NonceStyle.UnixMilliseconds;
19+
}
20+
21+
public override string Name => "Coinmate";
22+
23+
/// <summary>
24+
/// Coinmate private API requires a client id. Internally this is secured in the PassPhrase property.
25+
/// </summary>
26+
public string ClientId
27+
{
28+
get { return Passphrase.ToUnsecureString(); }
29+
set { Passphrase = value.ToSecureString(); }
30+
}
31+
32+
protected override async Task<ExchangeTicker> OnGetTickerAsync(string marketSymbol)
33+
{
34+
var response = await MakeCoinmateRequest<JToken>($"/ticker?currencyPair={marketSymbol}");
35+
return await this.ParseTickerAsync(response, marketSymbol, "ask", "bid", "last", "amount", null, "timestamp", TimestampType.UnixSeconds);
36+
}
37+
38+
protected override async Task<IEnumerable<string>> OnGetMarketSymbolsAsync()
39+
{
40+
var response = await MakeCoinmateRequest<CoinmateSymbol[]>("/products");
41+
return response.Select(x => $"{x.FromSymbol}{MarketSymbolSeparator}{x.ToSymbol}").ToArray();
42+
}
43+
44+
protected internal override async Task<IEnumerable<ExchangeMarket>> OnGetMarketSymbolsMetadataAsync()
45+
{
46+
var response = await MakeCoinmateRequest<CoinmateTradingPair[]>("/tradingPairs");
47+
return response.Select(x => new ExchangeMarket
48+
{
49+
IsActive = true,
50+
BaseCurrency = x.FirstCurrency,
51+
QuoteCurrency = x.SecondCurrency,
52+
MarketSymbol = x.Name,
53+
MinTradeSize = x.MinAmount,
54+
PriceStepSize = 1 / (decimal)(Math.Pow(10, x.PriceDecimals)),
55+
QuantityStepSize = 1 / (decimal)(Math.Pow(10, x.LotDecimals))
56+
}).ToArray();
57+
}
58+
59+
protected override async Task<ExchangeOrderBook> OnGetOrderBookAsync(string marketSymbol, int maxCount = 100)
60+
{
61+
var book = await MakeCoinmateRequest<CoinmateOrderBook>("/orderBook?&groupByPriceLimit=False&currencyPair=" + marketSymbol);
62+
var result = new ExchangeOrderBook
63+
{
64+
MarketSymbol = marketSymbol,
65+
};
66+
67+
book.Asks
68+
.GroupBy(x => x.Price)
69+
.ToList()
70+
.ForEach(x => result.Asks.Add(x.Key, new ExchangeOrderPrice { Amount = x.Sum(x => x.Amount), Price = x.Key }));
71+
72+
book.Bids
73+
.GroupBy(x => x.Price)
74+
.ToList()
75+
.ForEach(x => result.Bids.Add(x.Key, new ExchangeOrderPrice { Amount = x.Sum(x => x.Amount), Price = x.Key }));
76+
77+
return result;
78+
}
79+
80+
protected override async Task<IEnumerable<ExchangeTrade>> OnGetRecentTradesAsync(string marketSymbol, int? limit = null)
81+
{
82+
var txs = await MakeCoinmateRequest<CoinmateTransaction[]>("/transactions?minutesIntoHistory=1440&currencyPair=" + marketSymbol);
83+
return txs.Select(x => new ExchangeTrade
84+
{
85+
Amount = x.Amount,
86+
Id = x.TransactionId,
87+
IsBuy = x.TradeType == "BUY",
88+
Price = x.Price,
89+
Timestamp = CryptoUtility.ParseTimestamp(x.Timestamp, TimestampType.UnixMilliseconds)
90+
})
91+
.Take(limit ?? int.MaxValue)
92+
.ToArray();
93+
}
94+
95+
protected override async Task<Dictionary<string, decimal>> OnGetAmountsAsync()
96+
{
97+
var payload = await GetNoncePayloadAsync();
98+
var balances = await MakeCoinmateRequest<Dictionary<string, CoinmateBalance>>("/balances", payload, "POST");
99+
100+
return balances.ToDictionary(x => x.Key, x => x.Value.Balance);
101+
}
102+
103+
protected override async Task<Dictionary<string, decimal>> OnGetAmountsAvailableToTradeAsync()
104+
{
105+
var payload = await GetNoncePayloadAsync();
106+
var balances = await MakeCoinmateRequest<Dictionary<string, CoinmateBalance>>("/balances", payload, "POST");
107+
108+
return balances.ToDictionary(x => x.Key, x => x.Value.Available);
109+
}
110+
111+
protected override async Task<ExchangeOrderResult> OnGetOrderDetailsAsync(string orderId, string marketSymbol = null, bool isClientOrderId = false)
112+
{
113+
var payload = await GetNoncePayloadAsync();
114+
115+
CoinmateOrder o;
116+
117+
if (isClientOrderId)
118+
{
119+
payload["clientOrderId"] = orderId;
120+
var orders = await MakeCoinmateRequest<CoinmateOrder[]>("/order", payload, "POST");
121+
o = orders.OrderByDescending(x => x.Timestamp).FirstOrDefault();
122+
}
123+
else
124+
{
125+
payload["orderId"] = orderId;
126+
o = await MakeCoinmateRequest<CoinmateOrder>("/orderById", payload, "POST");
127+
}
128+
129+
if (o == null) return null;
130+
131+
return new ExchangeOrderResult
132+
{
133+
Amount = o.OriginalAmount,
134+
AmountFilled = o.OriginalAmount - o.RemainingAmount,
135+
AveragePrice = o.AvgPrice,
136+
ClientOrderId = isClientOrderId ? orderId : null,
137+
OrderId = o.Id.ToString(),
138+
Price = o.Price,
139+
IsBuy = o.Type == "BUY",
140+
OrderDate = CryptoUtility.ParseTimestamp(o.Timestamp, TimestampType.UnixMilliseconds),
141+
ResultCode = o.Status,
142+
Result = o.Status switch
143+
{
144+
"CANCELLED" => ExchangeAPIOrderResult.Canceled,
145+
"FILLED" => ExchangeAPIOrderResult.Filled,
146+
"PARTIALLY_FILLED" => ExchangeAPIOrderResult.FilledPartially,
147+
"OPEN" => ExchangeAPIOrderResult.Open,
148+
_ => ExchangeAPIOrderResult.Unknown
149+
},
150+
MarketSymbol = marketSymbol
151+
};
152+
}
153+
154+
protected override async Task OnCancelOrderAsync(string orderId, string marketSymbol = null)
155+
{
156+
var payload = await GetNoncePayloadAsync();
157+
payload["orderId"] = orderId;
158+
159+
await MakeCoinmateRequest<bool>("/cancelOrder", payload, "POST");
160+
}
161+
162+
protected override async Task<ExchangeOrderResult> OnPlaceOrderAsync(ExchangeOrderRequest order)
163+
{
164+
var payload = await GetNoncePayloadAsync();
165+
166+
if (order.OrderType != OrderType.Limit && order.OrderType != OrderType.Stop)
167+
{
168+
throw new NotImplementedException("This type of order is currently not supported.");
169+
}
170+
171+
payload["amount"] = order.Amount;
172+
payload["price"] = order.Price;
173+
payload["currencyPair"] = order.MarketSymbol;
174+
payload["postOnly"] = order.IsPostOnly.GetValueOrDefault() ? 1 : 0;
175+
176+
if (order.OrderType == OrderType.Stop)
177+
{
178+
payload["stopPrice"] = order.StopPrice;
179+
}
180+
181+
if (order.ClientOrderId != null)
182+
{
183+
if (!long.TryParse(order.ClientOrderId, out var clientOrderId))
184+
{
185+
throw new InvalidOperationException("ClientId must be numerical for Coinmate");
186+
}
187+
188+
payload["clientOrderId"] = clientOrderId;
189+
}
190+
191+
var url = order.IsBuy ? "/buyLimit" : "/sellLimit";
192+
var id = await MakeCoinmateRequest<long?>(url, payload, "POST");
193+
194+
try
195+
{
196+
return await GetOrderDetailsAsync(id?.ToString(), marketSymbol: order.MarketSymbol);
197+
}
198+
catch
199+
{
200+
return new ExchangeOrderResult { OrderId = id?.ToString() };
201+
}
202+
}
203+
204+
protected override async Task<IEnumerable<ExchangeOrderResult>> OnGetOpenOrderDetailsAsync(string marketSymbol = null)
205+
{
206+
var payload = await GetNoncePayloadAsync();
207+
payload["currencyPair"] = marketSymbol;
208+
209+
var orders = await MakeCoinmateRequest<CoinmateOpenOrder[]>("/openOrders", payload, "POST");
210+
211+
return orders.Select(x => new ExchangeOrderResult
212+
{
213+
Amount = x.Amount,
214+
ClientOrderId = x.ClientOrderId?.ToString(),
215+
IsBuy = x.Type == "BUY",
216+
MarketSymbol = x.CurrencyPair,
217+
OrderDate = CryptoUtility.ParseTimestamp(x.Timestamp, TimestampType.UnixMilliseconds),
218+
OrderId = x.Id.ToString(),
219+
Price = x.Price,
220+
221+
}).ToArray();
222+
}
223+
224+
protected override async Task<ExchangeDepositDetails> OnGetDepositAddressAsync(string currency, bool forceRegenerate = false)
225+
{
226+
var payload = await GetNoncePayloadAsync();
227+
var currencyName = GetCurrencyName(currency);
228+
var addresses = await MakeCoinmateRequest<string[]>($"/{currencyName}DepositAddresses", payload, "POST");
229+
230+
return new ExchangeDepositDetails
231+
{
232+
Address = addresses.FirstOrDefault(),
233+
Currency = currency,
234+
};
235+
}
236+
237+
protected override async Task<ExchangeWithdrawalResponse> OnWithdrawAsync(ExchangeWithdrawalRequest withdrawalRequest)
238+
{
239+
var payload = await GetNoncePayloadAsync();
240+
var currencyName = GetCurrencyName(withdrawalRequest.Currency);
241+
242+
payload["amount"] = withdrawalRequest.Amount;
243+
payload["address"] = withdrawalRequest.Address;
244+
payload["amountType"] = withdrawalRequest.TakeFeeFromAmount ? "NET" : "GROSS";
245+
246+
var id = await MakeCoinmateRequest<long?>($"/{currencyName}Withdrawal", payload, "POST");
247+
248+
return new ExchangeWithdrawalResponse
249+
{
250+
Id = id?.ToString(),
251+
Success = id != null
252+
};
253+
}
254+
255+
protected override async Task ProcessRequestAsync(IHttpWebRequest request, Dictionary<string, object> payload)
256+
{
257+
if (CanMakeAuthenticatedRequest(payload))
258+
{
259+
if (string.IsNullOrWhiteSpace(ClientId))
260+
{
261+
throw new APIException("Client ID is not set for Coinmate");
262+
}
263+
264+
var apiKey = PublicApiKey.ToUnsecureString();
265+
var messageToSign = payload["nonce"].ToStringInvariant() + ClientId + apiKey;
266+
var signature = CryptoUtility.SHA256Sign(messageToSign, PrivateApiKey.ToUnsecureString()).ToUpperInvariant();
267+
payload["signature"] = signature;
268+
payload["clientId"] = ClientId;
269+
payload["publicKey"] = apiKey;
270+
await CryptoUtility.WritePayloadFormToRequestAsync(request, payload);
271+
}
272+
}
273+
274+
private async Task<T> MakeCoinmateRequest<T>(string url, Dictionary<string, object> payload = null, string method = null)
275+
{
276+
var response = await MakeJsonRequestAsync<CoinmateResponse<T>>(url, null, payload, method);
277+
278+
if (response.Error)
279+
{
280+
throw new APIException(response.ErrorMessage);
281+
}
282+
283+
return response.Data;
284+
}
285+
286+
private string GetCurrencyName(string currency)
287+
{
288+
return currency.ToUpper() switch
289+
{
290+
"BTC" => "bitcoin",
291+
"LTC" => "litecoin",
292+
"BCH" => "bitcoinCash",
293+
"ETH" => "ethereum",
294+
"XRP" => "ripple",
295+
"DASH" => "dash",
296+
"DAI" => "dai",
297+
_ => throw new NotImplementedException("Unsupported currency")
298+
};
299+
}
300+
301+
public partial class ExchangeName { public const string Coinmate = "Coinmate"; }
302+
}
303+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace ExchangeSharp.API.Exchanges.Coinmate.Models
2+
{
3+
public class CoinmateBalance
4+
{
5+
public string Currency { get; set; }
6+
public decimal Balance { get; set; }
7+
public decimal Reserved { get; set; }
8+
public decimal Available { get; set; }
9+
}
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace ExchangeSharp.API.Exchanges.Coinmate.Models
2+
{
3+
public class CoinmateOpenOrder
4+
{
5+
public int Id { get; set; }
6+
public long Timestamp { get; set; }
7+
public string Type { get; set; }
8+
public string CurrencyPair { get; set; }
9+
public decimal Price { get; set; }
10+
public decimal Amount { get; set; }
11+
public decimal? StopPrice { get; set; }
12+
public string OrderTradeType { get; set; }
13+
public bool Hidden { get; set; }
14+
public bool Trailing { get; set; }
15+
public long? StopLossOrderId { get; set; }
16+
public long? ClientOrderId { get; set; }
17+
}
18+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace ExchangeSharp.API.Exchanges.Coinmate.Models
2+
{
3+
public class CoinmateOrder
4+
{
5+
public int Id { get; set; }
6+
public long Timestamp { get; set; }
7+
public string Type { get; set; }
8+
public decimal? Price { get; set; }
9+
public decimal? RemainingAmount { get; set; }
10+
public decimal OriginalAmount { get; set; }
11+
public decimal? StopPrice { get; set; }
12+
public string Status { get; set; }
13+
public string OrderTradeType { get; set; }
14+
public decimal? AvgPrice { get; set; }
15+
public bool Trailing { get; set; }
16+
public string StopLossOrderId { get; set; }
17+
public string OriginalOrderId { get; set; }
18+
}
19+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace ExchangeSharp.API.Exchanges.Coinmate.Models
2+
{
3+
public class CoinmateOrderBook
4+
{
5+
public AskBid[] Asks { get; set; }
6+
public AskBid[] Bids { get; set; }
7+
8+
public class AskBid
9+
{
10+
public decimal Price { get; set; }
11+
public decimal Amount { get; set; }
12+
}
13+
}
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ExchangeSharp.API.Exchanges.Coinmate.Models
2+
{
3+
public class CoinmateResponse<T>
4+
{
5+
public bool Error { get; set; }
6+
public string ErrorMessage { get; set; }
7+
public T Data { get; set; }
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace ExchangeSharp.API.Exchanges.Coinmate.Models
2+
{
3+
public class CoinmateSymbol
4+
{
5+
public string Id { get; set; }
6+
public string FromSymbol { get; set; }
7+
public string ToSymbol { get; set; }
8+
}
9+
}

0 commit comments

Comments
 (0)