Skip to content

Commit

Permalink
Merge pull request #35 from charvam/master
Browse files Browse the repository at this point in the history
Added Coinbase API connector.
  • Loading branch information
generalbytes authored Feb 19, 2021
2 parents b0fc6d2 + a71d246 commit 8bf5888
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 2 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
projectVersion=1.9.1
projectVersion=1.10.0
pf4jVersion=3.4.0
xchangeVersion=5.0.4
requiredEverytradeVersion=>=20201223
Expand Down
2 changes: 2 additions & 0 deletions plugin-base/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ dependencies {
pluginCompile "org.knowm.xchange:xchange-bitflyer:$xchangeVersion"
pluginCompile "org.knowm.xchange:xchange-core:$xchangeVersion"
pluginCompile "org.knowm.xchange:xchange-huobi:$xchangeVersion"
//TODO Change it to knowm when they accept GENERALBYTES pull-request.
pluginCompile "com.github.GENERALBYTESCOM.XChange:xchange-coinbase:045371c93518b63b16d44667b2035c8ee26975bf"

pluginCompile "com.github.charvam:V3-Open-API-SDK:52ae9f193f081aecd746f6d71e1b227ad18f17af"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.everytrade.server.plugin.impl.everytrade;

import io.everytrade.server.model.SupportedExchange;
import io.everytrade.server.plugin.api.IPlugin;
import io.everytrade.server.plugin.api.connector.ConnectorDescriptor;
import io.everytrade.server.plugin.api.connector.ConnectorParameterDescriptor;
import io.everytrade.server.plugin.api.connector.ConnectorParameterType;
import io.everytrade.server.plugin.api.connector.DownloadResult;
import io.everytrade.server.plugin.api.connector.IConnector;
import io.everytrade.server.plugin.api.parser.ParseResult;
import org.knowm.xchange.Exchange;
import org.knowm.xchange.ExchangeFactory;
import org.knowm.xchange.ExchangeSpecification;
import org.knowm.xchange.coinbase.v2.CoinbaseExchange;
import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeService;
import org.knowm.xchange.dto.trade.UserTrade;
import org.knowm.xchange.service.account.AccountService;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

public class CoinbaseConnector implements IConnector {
private static final String ID = EveryTradePlugin.ID + IPlugin.PLUGIN_PATH_SEPARATOR + "coinbaseApiConnector";
private static final SupportedExchange SUPPORTED_EXCHANGE = SupportedExchange.COINBASE;

private static final ConnectorParameterDescriptor PARAMETER_API_SECRET =
new ConnectorParameterDescriptor(
"apiSecret",
ConnectorParameterType.SECRET,
"API Secret",
""
);

private static final ConnectorParameterDescriptor PARAMETER_API_KEY =
new ConnectorParameterDescriptor(
"apiKey",
ConnectorParameterType.STRING,
"API Key",
""
);

public static final ConnectorDescriptor DESCRIPTOR = new ConnectorDescriptor(
ID,
"Coinbase Connector",
"",
SUPPORTED_EXCHANGE.getInternalId(),
List.of(PARAMETER_API_KEY, PARAMETER_API_SECRET)
);
private static final int REAL_WALLET_ID_LENGTH = 36;
private final String apiKey;
private final String apiSecret;

public CoinbaseConnector(Map<String, String> parameters) {
Objects.requireNonNull(this.apiKey = parameters.get(PARAMETER_API_KEY.getId()));
Objects.requireNonNull(this.apiSecret = parameters.get(PARAMETER_API_SECRET.getId()));
}

@Override
public String getId() {
return ID;
}

@Override
public DownloadResult getTransactions(String lastTransactionId) {
final ExchangeSpecification exSpec = new CoinbaseExchange().getDefaultExchangeSpecification();
exSpec.setApiKey(apiKey);
exSpec.setSecretKey(apiSecret);
final Exchange exchange = ExchangeFactory.INSTANCE.createExchange(exSpec);
final CoinbaseTradeService tradeService = (CoinbaseTradeService) exchange.getTradeService();
final AccountService accountService = exchange.getAccountService();
final Set<String> walletIds;
try {
walletIds = filterWalletIds(accountService.getAccountInfo().getWallets().keySet());
} catch (IOException e) {
throw new IllegalStateException("Wallets download failed.", e);
}
final CoinbaseDownloader coinbaseDownloader
= new CoinbaseDownloader(tradeService, lastTransactionId, walletIds);
final List<UserTrade> userTrades = coinbaseDownloader.download();
final ParseResult parseResult = XChangeConnectorParser.getParseResult(userTrades, SUPPORTED_EXCHANGE);

return new DownloadResult(parseResult, coinbaseDownloader.getLastTransactionId());
}

private Set<String> filterWalletIds(Set<String> walletsIds) {
return walletsIds
.stream()
.filter(s -> s.length() == REAL_WALLET_ID_LENGTH)
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.everytrade.server.plugin.impl.everytrade;

import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeHistoryParams;
import org.knowm.xchange.coinbase.v2.service.CoinbaseTradeService;
import org.knowm.xchange.dto.trade.UserTrade;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static org.knowm.xchange.bitfinex.service.BitfinexAdapters.log;

public class CoinbaseDownloader {
//https://developers.coinbase.com/api/v2#rate-limiting 10.000 / API-KEY / hour ---> 2500 / 15 min
private static final int MAX_REQUEST_COUNT = 2500;
private static final String DASH_SYMBOL = "-";
private static final String COLON_SYMBOL = ":";
private static final String PIPE_SYMBOL = "|";
private static final int TRANSACTIONS_PER_REQUEST_LIMIT = 100;
private final CoinbaseTradeService tradeService;
private final Map<String, WalletState> actualWalletStates = new HashMap<>();

public CoinbaseDownloader(
CoinbaseTradeService tradeService,
String lastTransactionId,
Set<String> actualWalletIds
) {
Objects.requireNonNull(this.tradeService = tradeService);
Map<String, WalletState> previousWalletStates;
if (lastTransactionId == null) {
previousWalletStates = new HashMap<>();
} else {
previousWalletStates = Arrays.stream(lastTransactionId.split("\\" + PIPE_SYMBOL))
.map(entry -> entry.split(COLON_SYMBOL))
.collect(Collectors.toMap(entry -> entry[0], entry -> new WalletState(entry[1], entry[2])));
}

for (String actualWalletId : actualWalletIds) {
final WalletState walletState = previousWalletStates.get(actualWalletId);
actualWalletStates.put(actualWalletId, Objects.requireNonNullElseGet(
walletState,
() -> new WalletState(null, null))
);
}
}

public List<UserTrade> download() {
final List<UserTrade> userTrades = new ArrayList<>();
int sentRequests = 0;
final CoinbaseTradeHistoryParams tradeHistoryParams
= (CoinbaseTradeHistoryParams) tradeService.createTradeHistoryParams();
tradeHistoryParams.setLimit(TRANSACTIONS_PER_REQUEST_LIMIT);

for (Map.Entry<String, WalletState> entry : actualWalletStates.entrySet()) {
String lastBuyId = entry.getValue().lastBuyId;
String lastSellId = entry.getValue().lastSellId;
final String walletId = entry.getKey();

while (sentRequests < MAX_REQUEST_COUNT) {
++sentRequests;
tradeHistoryParams.setStartId(lastBuyId);
final List<UserTrade> buysTradeHistoryBlock;
try {
buysTradeHistoryBlock
= tradeService.getBuyTradeHistory(tradeHistoryParams, walletId).getUserTrades();
} catch (IOException e) {
throw new IllegalStateException("Download buys history failed.", e);
}

if (buysTradeHistoryBlock.isEmpty()) {
break;
}

userTrades.addAll(buysTradeHistoryBlock);
lastBuyId = buysTradeHistoryBlock.get(0).getId();

}

while (sentRequests < MAX_REQUEST_COUNT) {
++sentRequests;
tradeHistoryParams.setStartId(lastSellId);
final List<UserTrade> sellsTradeHistoryBlock;
try {
sellsTradeHistoryBlock
= tradeService.getSellTradeHistory(tradeHistoryParams, walletId).getUserTrades();
} catch (IOException e) {
throw new IllegalStateException("Download sells history failed.", e);
}

if (sellsTradeHistoryBlock.isEmpty()) {
break;
}

userTrades.addAll(sellsTradeHistoryBlock);
lastSellId = sellsTradeHistoryBlock.get(0).getId();

}
if (sentRequests == MAX_REQUEST_COUNT) {
log.info("Max request count {} has been achieved.", MAX_REQUEST_COUNT);
}

final WalletState walletState = actualWalletStates.get(walletId);
walletState.lastBuyId = lastBuyId;
walletState.lastSellId = lastSellId;

}
return userTrades;
}

public String getLastTransactionId() {
final String result = actualWalletStates.entrySet().stream()
.filter(entry -> entry.getValue().lastBuyId != null || entry.getValue().lastSellId != null)
.map(
entry -> entry.getKey()
+ COLON_SYMBOL
+ Objects.requireNonNullElse(entry.getValue().lastBuyId, DASH_SYMBOL)
+ COLON_SYMBOL
+ Objects.requireNonNullElse(entry.getValue().lastSellId, DASH_SYMBOL)
)
.collect(Collectors.joining(PIPE_SYMBOL));

return result;
}

private static class WalletState {
private String lastBuyId;
private String lastSellId;

public WalletState(String lastBuyId, String lastSellId) {
this.lastBuyId = DASH_SYMBOL.equals(lastBuyId) ? null : lastBuyId;
this.lastSellId = DASH_SYMBOL.equals(lastSellId) ? null : lastSellId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class EveryTradePlugin implements IPlugin {
CoinbaseProConnector.DESCRIPTOR,
BitmexConnector.DESCRIPTOR,
OkexConnector.DESCRIPTOR,
HuobiConnector.DESCRIPTOR
HuobiConnector.DESCRIPTOR,
CoinbaseConnector.DESCRIPTOR
).stream()
.collect(Collectors.toMap(ConnectorDescriptor::getId, it -> it));

Expand Down Expand Up @@ -93,6 +94,9 @@ public IConnector createConnectorInstance(String connectorId, Map<String, String
if (connectorId.equals(HuobiConnector.DESCRIPTOR.getId())) {
return new HuobiConnector(parameters);
}
if (connectorId.equals(CoinbaseConnector.DESCRIPTOR.getId())) {
return new CoinbaseConnector(parameters);
}
return null;
}

Expand Down

0 comments on commit 8bf5888

Please sign in to comment.