Skip to content

Commit

Permalink
Merge pull request #870 from Wuchte/stillman_integration
Browse files Browse the repository at this point in the history
Stillman Digital LLC integration
  • Loading branch information
generalbytes authored Nov 27, 2023
2 parents c5b711d + 4279acb commit 5cfee99
Show file tree
Hide file tree
Showing 18 changed files with 863 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.hitbtc.HitbtcExchange;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.enigma.EnigmaExchange;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.poloniex.PoloniexExchange;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital.StillmanDigitalExchange;
import com.generalbytes.batm.server.extensions.extra.bitcoin.paymentprocessors.bitcoinpay.BitcoinPayPP;
import com.generalbytes.batm.server.extensions.extra.bitcoin.paymentprocessors.coinofsale.CoinOfSalePP;
import com.generalbytes.batm.server.extensions.extra.bitcoin.sources.bitkub.BitKubRateSource;
Expand Down Expand Up @@ -221,6 +222,11 @@ public IExchange createExchange(String paramString) {
String apiKey = paramTokenizer.nextToken();
String apiSecret = paramTokenizer.nextToken();
return new BitbuyExchange(apiKey, apiSecret);
} else if ("stillmandigital".equalsIgnoreCase(prefix)) {
String apiKey = paramTokenizer.nextToken();
String apiSecret = paramTokenizer.nextToken();
boolean useSandbox = paramTokenizer.hasMoreTokens() && paramTokenizer.nextToken().equals("sandbox");
return new StillmanDigitalExchange(apiKey, apiSecret, useSandbox);
}
}
} catch (Exception e) {
Expand Down Expand Up @@ -585,6 +591,11 @@ public IRateSource createRateSource(String sourceLogin) {
String apiKey = st.nextToken();
String apiSecret = st.nextToken();
return new BitbuyExchange(apiKey, apiSecret);
} else if ("stillmandigital".equalsIgnoreCase(rsType)) {
String apiKey = st.nextToken();
String apiSecret = st.nextToken();
boolean useSandbox = st.hasMoreTokens() && st.nextToken().equals("sandbox");
return new StillmanDigitalExchange(apiKey, apiSecret, useSandbox);
}
}
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*************************************************************************************
* Copyright (C) 2014-2023 GENERAL BYTES s.r.o. All rights reserved.
*
* This software may be distributed and modified under the terms of the GNU
* General Public License version 2 (GPL2) as published by the Free Software
* Foundation and appearing in the file GPL2.TXT included in the packaging of
* this file. Please note that GPL2 Section 2[b] requires that all works based
* on this software must also be made publicly available under the terms of
* the GPL2 ("Copyleft").
*
* Contact information
* -------------------
*
* GENERAL BYTES s.r.o.
* Web : http://www.generalbytes.com
*
************************************************************************************/
package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital;

import si.mazi.rescu.SynchronizedValueFactory;

import java.time.Clock;
import java.util.concurrent.TimeUnit;

public class CurrentTimeFactory implements SynchronizedValueFactory<Long> {

private static final Clock UTC_CLOCK = Clock.systemUTC();


@Override
public Long createValue() {
return TimeUnit.MILLISECONDS.toSeconds(UTC_CLOCK.millis());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*************************************************************************************
* Copyright (C) 2014-2023 GENERAL BYTES s.r.o. All rights reserved.
*
* This software may be distributed and modified under the terms of the GNU
* General Public License version 2 (GPL2) as published by the Free Software
* Foundation and appearing in the file GPL2.TXT included in the packaging of
* this file. Please note that GPL2 Section 2[b] requires that all works based
* on this software must also be made publicly available under the terms of
* the GPL2 ("Copyleft").
*
* Contact information
* -------------------
*
* GENERAL BYTES s.r.o.
* Web : http://www.generalbytes.com
*
************************************************************************************/
package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital;

import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital.dto.*;
import com.generalbytes.batm.server.extensions.util.net.RateLimitingInterceptor;
import si.mazi.rescu.ClientConfig;
import si.mazi.rescu.Interceptor;
import si.mazi.rescu.RestProxyFactory;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.List;

@Path("")
@Produces(MediaType.APPLICATION_JSON)
public interface IStillmanDigitalAPI {

String API_EXPIRES_HEADER = "api-timestamp";

static IStillmanDigitalAPI create(String apiKey, String apiSecret,
boolean useSandbox) throws GeneralSecurityException {
return create(apiKey, apiSecret,
useSandbox ? "https://sandbox-api.stillmandigital.com" : "https://api.stillmandigital.com");
}

static IStillmanDigitalAPI create(String apiKey, String apiSecret, String baseUrl) throws GeneralSecurityException {
final ClientConfig config = new ClientConfig();
config.addDefaultParam(HeaderParam.class, "api-key", apiKey);
config.addDefaultParam(HeaderParam.class, API_EXPIRES_HEADER, new CurrentTimeFactory());
config.addDefaultParam(HeaderParam.class, "api-signature", new StillmanDigitalDigest(apiSecret));
Interceptor interceptor = new RateLimitingInterceptor(IStillmanDigitalAPI.class, 25, 30_000);
return RestProxyFactory.createProxy(IStillmanDigitalAPI.class, baseUrl, config, interceptor);
}

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("/v1/trading/rate")
Rate requestRate(RateRequest request) throws IOException;

@GET
@Path("/v1/balances")
List<RowBalanceByAssetResponse> getBalance() throws IOException;

@POST
@Consumes(MediaType.APPLICATION_JSON)
@Path("/v1/trading/new")
NewOrderResponse sendOrder(OrderRequest orderRequest) throws IOException;

@GET
@Path("/v1/orders/{id}")
RowOrderResponse getOrder(@PathParam("id") long orderId) throws IOException;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*************************************************************************************
* Copyright (C) 2014-2023 GENERAL BYTES s.r.o. All rights reserved.
*
* This software may be distributed and modified under the terms of the GNU
* General Public License version 2 (GPL2) as published by the Free Software
* Foundation and appearing in the file GPL2.TXT included in the packaging of
* this file. Please note that GPL2 Section 2[b] requires that all works based
* on this software must also be made publicly available under the terms of
* the GPL2 ("Copyleft").
*
* Contact information
* -------------------
*
* GENERAL BYTES s.r.o.
* Web : http://www.generalbytes.com
*
************************************************************************************/
package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital;

import com.generalbytes.batm.server.coinutil.Hex;
import si.mazi.rescu.ParamsDigest;
import si.mazi.rescu.RestInvocation;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;

public class StillmanDigitalDigest implements ParamsDigest {

private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
private static final Charset CHARSET = StandardCharsets.UTF_8;

private final Mac mac;

public StillmanDigitalDigest(String apiSecret) throws GeneralSecurityException {
mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
mac.init(new SecretKeySpec(apiSecret.getBytes(CHARSET), HMAC_SHA256_ALGORITHM));
}

public String digestParams(RestInvocation restInvocation) {
// String dataForSign = method + BALANCE_URL_PART + validUntilSeconds + body;
String dataForSign = restInvocation.getHttpMethod()
+ restInvocation.getMethodPath()
+ restInvocation.getHttpHeadersFromParams().get(IStillmanDigitalAPI.API_EXPIRES_HEADER)
+ restInvocation.getRequestBody();
return signHmacSha256(dataForSign);
}

private String signHmacSha256(String data) {
byte[] signData = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));

return Hex.bytesToHexString(signData);
}
}




Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/*************************************************************************************
* Copyright (C) 2014-2023 GENERAL BYTES s.r.o. All rights reserved.
*
* This software may be distributed and modified under the terms of the GNU
* General Public License version 2 (GPL2) as published by the Free Software
* Foundation and appearing in the file GPL2.TXT included in the packaging of
* this file. Please note that GPL2 Section 2[b] requires that all works based
* on this software must also be made publicly available under the terms of
* the GPL2 ("Copyleft").
*
* Contact information
* -------------------
*
* GENERAL BYTES s.r.o.
* Web : http://www.generalbytes.com
*
************************************************************************************/
package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital;

import com.generalbytes.batm.common.currencies.CryptoCurrency;
import com.generalbytes.batm.common.currencies.FiatCurrency;
import com.generalbytes.batm.server.extensions.IExchangeAdvanced;
import com.generalbytes.batm.server.extensions.IRateSourceAdvanced;
import com.generalbytes.batm.server.extensions.ITask;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital.dto.RateRequest;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital.dto.RowBalanceByAssetResponse;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital.dto.Side;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.stillmandigital.dto.Rate;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.math.BigDecimal;
import java.security.GeneralSecurityException;
import java.util.Objects;
import java.util.Set;

public class StillmanDigitalExchange implements IExchangeAdvanced, IRateSourceAdvanced {
private static final Logger log = LoggerFactory.getLogger("batm.master.exchange.StillmanDigitalExchange");
public static final String SEPARATOR = "/";

private final String preferredFiatCurrency = FiatCurrency.USD.getCode();
private final IStillmanDigitalAPI api;

public StillmanDigitalExchange(String apiKey,
String apiSecret,
boolean useSandbox) throws GeneralSecurityException {
this.api = IStillmanDigitalAPI.create(apiKey, apiSecret, useSandbox);
}

// for tests only
StillmanDigitalExchange(String apiKey,
String apiSecret,
String baseUrl) throws GeneralSecurityException {
this.api = IStillmanDigitalAPI.create(apiKey, apiSecret, baseUrl);
}

private static final Set<String> fiatCurrencies = ImmutableSet.of(
FiatCurrency.USD.getCode());

private static final Set<String> cryptoCurrencies = ImmutableSet.of(
CryptoCurrency.BTC.getCode(),
CryptoCurrency.ETH.getCode());

@Override
public Set<String> getCryptoCurrencies() {
return cryptoCurrencies;
}

@Override
public Set<String> getFiatCurrencies() {
return fiatCurrencies;
}

@Override
public String getPreferredFiatCurrency() {
return preferredFiatCurrency;
}

@Override
public BigDecimal getCryptoBalance(String cryptoCurrency) {
try {
for (RowBalanceByAssetResponse assetData : api.getBalance()) {
if (Objects.equals(cryptoCurrency, assetData.asset)) {
// crypto is interesting in terms on how much client can withdraw
return assetData.total;
}
}
} catch (IOException e) {
log.error("Error", e);
}
return null;
}

@Override
public BigDecimal getFiatBalance(String fiatCurrency) {
try {
for (RowBalanceByAssetResponse assetData : api.getBalance()) {
if (Objects.equals(fiatCurrency, assetData.asset)) {
// fiat is interesting in terms on how much client can spent to buy crypto, due this just FREE
return assetData.free;
}
}
} catch (IOException e) {
log.error("Error", e);
}
return null;
}

@Override
public String sendCoins(String destinationAddress,
BigDecimal amount, String cryptoCurrency, String description) {
return "Plz contact your manager for withdraw";
}

@Override
public String getDepositAddress(String cryptoCurrency) {
return null;
}

@Override
public ITask createPurchaseCoinsTask(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) {
return new StillmanOrderTask(api, Side.BUY, cryptoCurrency + SEPARATOR + fiatCurrencyToUse, amount, log);
}

@Override
public ITask createSellCoinsTask(BigDecimal amount, String cryptoCurrency, String fiatCurrencyToUse, String description) {
return new StillmanOrderTask(api, Side.SELL, cryptoCurrency + SEPARATOR + fiatCurrencyToUse, amount, log);
}

@Override
public BigDecimal getExchangeRateForBuy(String cryptoCurrency, String fiatCurrency) {
try {
Rate rate = api.requestRate(new RateRequest(cryptoCurrency + SEPARATOR + fiatCurrency));
if (rate != null) {
return rate.buyRate;
}
} catch (IOException e) {
log.error("Error", e);
}
return null;
}

@Override
public BigDecimal getExchangeRateForSell(String cryptoCurrency, String fiatCurrency) {
try {
Rate rate = api.requestRate(new RateRequest(cryptoCurrency + SEPARATOR + fiatCurrency));
if (rate != null) {
return rate.sellRate;
}
} catch (IOException e) {
log.error("Error", e);
}
return null;
}

@Override
public BigDecimal calculateBuyPrice(String cryptoCurrency, String fiatCurrency, BigDecimal cryptoAmount) {
try {
Rate rate = api.requestRate(new RateRequest(cryptoCurrency + SEPARATOR + fiatCurrency, cryptoAmount));
if (rate != null && rate.buyRate != null) {
return rate.buyRate;
}
} catch (IOException e) {
log.error("Error", e);
}
return null;
}

@Override
public BigDecimal calculateSellPrice(String cryptoCurrency, String fiatCurrency, BigDecimal cryptoAmount) {
try {
Rate rate = api.requestRate(new RateRequest(cryptoCurrency + SEPARATOR + fiatCurrency, cryptoAmount));
if (rate != null && rate.sellRate != null) {
return rate.sellRate;
}
} catch (IOException e) {
log.error("Error", e);
}
return null;
}
}
Loading

0 comments on commit 5cfee99

Please sign in to comment.