From 25dbdd40a1575584209b03d4d259b789b3415f2b Mon Sep 17 00:00:00 2001
From: Premek
Date: Tue, 25 Jul 2023 10:22:09 +0200
Subject: [PATCH 01/82] BATM-5098 Bitbuy (#826)
---
.../extra/bitcoin/BitcoinExtension.java | 12 +-
.../bitcoin/exchanges/XChangeExchange.java | 54 +++------
.../exchanges/bitbuy/BitbuyDigest.java | 44 --------
.../exchanges/bitbuy/BitbuyExchange.java | 103 +++++++-----------
.../exchanges/bitbuy/BitbuyMacData.java | 30 -----
.../bitcoin/exchanges/bitbuy/IBitbuyAPI.java | 77 +++++--------
.../bitcoin/exchanges/bitbuy/dto/Balance.java | 9 ++
.../exchanges/bitbuy/dto/BitbuyResponse.java | 8 ++
.../bitcoin/exchanges/bitbuy/dto/Coin.java | 11 --
.../bitbuy/dto/CreateOrderResponse.java | 6 +
.../exchanges/bitbuy/dto/CurrencySide.java | 6 +
.../exchanges/bitbuy/dto/DepositAddress.java | 5 -
.../bitcoin/exchanges/bitbuy/dto/Market.java | 12 --
.../exchanges/bitbuy/dto/OrderBook.java | 9 ++
.../exchanges/bitbuy/dto/OrderBookLevel.java | 9 ++
.../exchanges/bitbuy/dto/OrderRequest.java | 33 ------
.../exchanges/bitbuy/dto/OrderResponse.java | 32 ------
.../exchanges/bitbuy/dto/OrderStatus.java | 8 ++
.../exchanges/bitbuy/dto/Paginated.java | 14 +++
.../exchanges/bitbuy/dto/QuoteRequest.java | 30 ++---
.../exchanges/bitbuy/dto/QuoteResponse.java | 21 ++--
.../bitcoin/exchanges/bitbuy/dto/Wallet.java | 20 ----
.../exchanges/bitbuy/dto/WithdrawResult.java | 10 --
.../util/OrderBookPriceCalculator.java | 73 +++++++++++++
.../src/main/resources/batm-extensions.xml | 6 -
.../exchanges/bitbuy/BitbuyDigestTest.java | 56 ----------
.../util/OrderBookPriceCalculatorTest.java | 59 ++++++++++
27 files changed, 309 insertions(+), 448 deletions(-)
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigest.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyMacData.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Balance.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/BitbuyResponse.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Coin.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CreateOrderResponse.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CurrencySide.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Market.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBook.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBookLevel.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderRequest.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderResponse.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderStatus.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Paginated.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Wallet.java
delete mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/WithdrawResult.java
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculator.java
delete mode 100644 server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigestTest.java
create mode 100644 server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculatorTest.java
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java
index 67ffc0982..27f0b6d86 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/BitcoinExtension.java
@@ -220,11 +220,7 @@ public IExchange createExchange(String paramString) {
} else if ("bitbuy".equalsIgnoreCase(prefix)) {
String apiKey = paramTokenizer.nextToken();
String apiSecret = paramTokenizer.nextToken();
- String preferredFiatCurrency = FiatCurrency.CAD.getCode();
- if (paramTokenizer.hasMoreTokens()) {
- preferredFiatCurrency = paramTokenizer.nextToken().toUpperCase();
- }
- return new BitbuyExchange(apiKey, apiSecret, preferredFiatCurrency);
+ return new BitbuyExchange(apiKey, apiSecret);
}
}
} catch (Exception e) {
@@ -588,11 +584,7 @@ public IRateSource createRateSource(String sourceLogin) {
} else if ("bitbuy".equalsIgnoreCase(rsType)) {
String apiKey = st.nextToken();
String apiSecret = st.nextToken();
- String preferredFiatCurrency = FiatCurrency.CAD.getCode();
- if (st.hasMoreTokens()) {
- preferredFiatCurrency = st.nextToken().toUpperCase();
- }
- return new BitbuyExchange(apiKey, apiSecret, preferredFiatCurrency);
+ return new BitbuyExchange(apiKey, apiSecret);
}
}
} catch (Exception e) {
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java
index 2c01b86f8..40578a1ba 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java
@@ -21,6 +21,7 @@
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.util.OrderBookPriceCalculator;
import com.generalbytes.batm.server.extensions.util.net.RateLimiter;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
@@ -50,8 +51,6 @@
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -61,8 +60,7 @@
import static java.util.Comparator.comparing;
public abstract class XChangeExchange implements IExchangeAdvanced, IRateSourceAdvanced {
- private static final Comparator asksComparator = comparing(LimitOrder::getLimitPrice);
- private static final Comparator bidsComparator = comparing(LimitOrder::getLimitPrice).reversed();
+ private static final OrderBookPriceCalculator orderBookPriceCalculator = new OrderBookPriceCalculator<>(LimitOrder::getLimitPrice, LimitOrder::getOriginalAmount);
private String preferredFiatCurrency;
private static final long cacheRefreshSeconds = 30;
@@ -262,10 +260,9 @@ public String purchaseCoins(BigDecimal amount, String cryptoCurrency, String fia
OrderBook orderBook = marketDataService.getOrderBook(currencyPair);
List asks = orderBook.getAsks();
-
- Collections.sort(asks, asksComparator);
-
- LimitOrder order = new LimitOrder(Order.OrderType.BID, getTradableAmount(amount, currencyPair), currencyPair, "", null, getTradablePrice(amount, asks));
+ BigDecimal tradablePrice = orderBookPriceCalculator.getBuyPrice(amount, asks);
+ log.debug("tradablePrice: {}", tradablePrice);
+ LimitOrder order = new LimitOrder(Order.OrderType.BID, getTradableAmount(amount, currencyPair), currencyPair, "", null, tradablePrice);
log.debug("order = {}", order);
RateLimiter.waitForPossibleCall(getClass());
String orderId = tradeService.placeLimitOrder(order);
@@ -326,26 +323,6 @@ public ITask createPurchaseCoinsTask(BigDecimal amount, String cryptoCurrency, S
return new PurchaseCoinsTask(amount, cryptoCurrency, fiatCurrencyToUse, description);
}
- /**
- *
- * @param cryptoAmount
- * @param bidsOrAsksSorted bids: highest first, asks: lowest first
- * @return
- * @throws IOException when tradable price not found, e.g orderbook not received or too small.
- */
- private BigDecimal getTradablePrice(BigDecimal cryptoAmount, List bidsOrAsksSorted) throws IOException {
- BigDecimal total = BigDecimal.ZERO;
-
- for (LimitOrder order : bidsOrAsksSorted) {
- total = total.add(order.getOriginalAmount());
- if (cryptoAmount.compareTo(total) <= 0) {
- log.debug("tradablePrice: {}", order.getLimitPrice());
- return order.getLimitPrice();
- }
- }
- throw new IOException("tradable price not available");
- }
-
@Override
public String getDepositAddress(String cryptoCurrency) {
if (cryptoCurrency == null) {
@@ -404,10 +381,9 @@ public String sellCoins(BigDecimal cryptoAmount, String cryptoCurrency, String f
List bids = orderBook.getBids();
log.debug("bids.size(): {}", bids.size());
- Collections.sort(bids, bidsComparator);
-
- LimitOrder order = new LimitOrder(Order.OrderType.ASK, getTradableAmount(cryptoAmount, currencyPair), currencyPair,
- "", null, getTradablePrice(cryptoAmount, bids));
+ BigDecimal tradablePrice = orderBookPriceCalculator.getSellPrice(cryptoAmount, bids);
+ log.debug("tradablePrice: {}", tradablePrice);
+ LimitOrder order = new LimitOrder(Order.OrderType.ASK, getTradableAmount(cryptoAmount, currencyPair), currencyPair, "", null, tradablePrice);
log.debug("order: {}", order);
RateLimiter.waitForPossibleCall(getClass());
@@ -628,10 +604,9 @@ public boolean onCreate() {
OrderBook orderBook = marketDataService.getOrderBook(currencyPair);
List asks = orderBook.getAsks();
- asks.sort(asksComparator);
-
- LimitOrder order = new LimitOrder(Order.OrderType.BID, getTradableAmount(amount, currencyPair), currencyPair, "", null,
- getTradablePrice(amount, asks));
+ BigDecimal tradablePrice = orderBookPriceCalculator.getBuyPrice(amount, asks);
+ log.debug("tradablePrice: {}", tradablePrice);
+ LimitOrder order = new LimitOrder(Order.OrderType.BID, getTradableAmount(amount, currencyPair), currencyPair, "", null, tradablePrice);
log.debug("limitOrder = {}", order);
@@ -759,10 +734,9 @@ public boolean onCreate() {
List bids = orderBook.getBids();
log.debug("bids.size(): {}", bids.size());
- Collections.sort(bids, bidsComparator);
-
- LimitOrder order = new LimitOrder(Order.OrderType.ASK, getTradableAmount(cryptoAmount, currencyPair), currencyPair,
- "", null, getTradablePrice(cryptoAmount, bids));
+ BigDecimal tradablePrice = orderBookPriceCalculator.getSellPrice(cryptoAmount, bids);
+ log.debug("tradablePrice: {}", tradablePrice);
+ LimitOrder order = new LimitOrder(Order.OrderType.ASK, getTradableAmount(cryptoAmount, currencyPair), currencyPair, "", null, tradablePrice);
log.debug("order = {}", order);
RateLimiter.waitForPossibleCall(getClass());
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigest.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigest.java
deleted file mode 100644
index 38a277680..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-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;
-import java.util.Base64;
-
-public class BitbuyDigest implements ParamsDigest {
-
- private static final String ALGORITHM = "HmacSHA256";
- private static final Charset CHARSET = StandardCharsets.UTF_8;
-
- private final Mac mac;
-
- public BitbuyDigest(String apiSecret) throws GeneralSecurityException {
- this.mac = Mac.getInstance(ALGORITHM);
- this.mac.init(new SecretKeySpec(apiSecret.getBytes(CHARSET), ALGORITHM));
- }
-
- public String digestParams(RestInvocation restInvocation) {
- byte[] data = getMacData(restInvocation);
- byte[] signature = mac.doFinal(data);
- return Base64.getEncoder().encodeToString(signature);
- }
-
- protected byte[] getMacData(RestInvocation restInvocation) {
- try {
- return new ObjectMapper().writer().writeValueAsBytes(BitbuyMacData.from(restInvocation));
- } catch (JsonProcessingException e) {
- throw new RuntimeException(e);
- }
- }
-}
-
-
-
-
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java
index e0d2370ce..3e24f5410 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java
@@ -22,46 +22,40 @@
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.bitbuy.dto.OrderRequest;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderResponse;
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderBook;
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderBookLevel;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderSide;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderType;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.QuoteRequest;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.QuoteResponse;
+import com.generalbytes.batm.server.extensions.util.OrderBookPriceCalculator;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import si.mazi.rescu.HttpStatusIOException;
import java.math.BigDecimal;
-import java.security.GeneralSecurityException;
+import java.math.RoundingMode;
import java.util.Set;
import java.util.concurrent.Callable;
public class BitbuyExchange implements IExchangeAdvanced, IRateSourceAdvanced {
private static final Logger log = LoggerFactory.getLogger("batm.master.exchange.BitbuyExchange");
-
- private static final Set fiatCurrencies = ImmutableSet.of(
- FiatCurrency.CAD.getCode(),
- CryptoCurrency.DAI.getCode()); // stable coin as fiat
-
+ private static final OrderBookPriceCalculator orderBookPriceCalculator = new OrderBookPriceCalculator<>(o -> o.pricePerUnit, o -> o.quantity);
+ private static final int orderBookDepth = 200;
+ private static final String preferredFiatCurrency = FiatCurrency.CAD.getCode();
+ private static final Set fiatCurrencies = ImmutableSet.of(FiatCurrency.CAD.getCode());
private static final Set cryptoCurrencies = ImmutableSet.of(
CryptoCurrency.BCH.getCode(),
CryptoCurrency.BTC.getCode(),
- CryptoCurrency.DAI.getCode(),
CryptoCurrency.ETH.getCode(),
- CryptoCurrency.LTC.getCode(),
- CryptoCurrency.XRP.getCode());
+ CryptoCurrency.LTC.getCode());
- // Supported markets (2021-10-06; see IBitbuyAPI.getMarkets):
- // [DAI-CAD, ETH-DAI, BTC-AAVE, ETH-AAVE, ETH-CAD, BTC-LINK, ETH-LINK, BCH-CAD, AAVE-CAD, BCH-BTC, BTC-DAI, LINK-CAD, BTC-CAD, XLM-CAD, LTC-CAD, XRP-CAD, EOS-BTC, XLM-BTC, XRP-BTC, ETH-BTC, LTC-BTC, EOS-CAD]
+ // Supported markets (2022-07-17) $ curl https://api-crypto.bitbuy.ca/public/markets | jq -r '.data|.[].symbol' | sort
+ // AAVE-CAD, ADA-CAD, APE-CAD, AXS-CAD, BAT-CAD, BCH-CAD, BTC-CAD, COMP-CAD, CRV-CAD, DOGE-CAD, DOT-CAD, EOS-CAD, ETH-CAD, FTM-CAD, LINK-CAD, LTC-CAD, MANA-CAD, MATIC-CAD, MKR-CAD, SOL-CAD, SUSHI-CAD, UNI-CAD, USDC-CAD, XLM-CAD
- private final String preferredFiatCurrency;
private final IBitbuyAPI api;
- public BitbuyExchange(String apiKey, String apiSecret, String preferredFiatCurrency) throws GeneralSecurityException {
- this.api = IBitbuyAPI.create(apiKey, apiSecret);
- this.preferredFiatCurrency = preferredFiatCurrency;
+ public BitbuyExchange(String clientId, String secretKey) {
+ this.api = IBitbuyAPI.create(clientId, secretKey);
}
private String getMarketSymbol(String cryptoCurrency, String fiatCurrency) {
@@ -101,13 +95,7 @@ public BigDecimal getFiatBalance(String fiatCurrency) {
}
private BigDecimal getBalance(String currency) {
- return call(currency + " balance", () ->
- api.getWallets().stream()
- .filter(w -> currency.equals(w.symbol))
- .findAny()
- .map(w -> w.availableBalance)
- .orElseThrow(() -> new Exception(currency + " wallet not found"))
- );
+ return call(currency + " balance", () -> api.getBalance(currency).data.available);
}
@Override
@@ -116,17 +104,13 @@ public String getDepositAddress(String cryptoCurrency) {
return null;
}
- return call("deposit address", () -> api.getDepositAddress(cryptoCurrency).address);
+ return call("deposit address", () -> api.getDepositAddress(cryptoCurrency).data.address);
}
@Override
public String sendCoins(String destinationAddress, BigDecimal amount, String cryptoCurrency, String description) {
- if (!isCryptoCurrencySupported(cryptoCurrency)) {
- return null;
- }
-
- log.info("withdrawing {} {} to {}", amount, cryptoCurrency, destinationAddress);
- return call("send coins", () -> api.withdraw(cryptoCurrency, destinationAddress, amount).transactionReference);
+ log.info("Sending coins from this exchange is not supported. Configure a different strategy in your crypto settings please");
+ return null;
}
@Override
@@ -158,7 +142,7 @@ public BigDecimal getExchangeRateForBuy(String cryptoCurrency, String fiatCurren
BigDecimal rateSourceCryptoVolume = getRateSourceCryptoVolume(cryptoCurrency);
BigDecimal result = calculateBuyPrice(cryptoCurrency, fiatCurrency, rateSourceCryptoVolume);
if (result != null) {
- return result.divide(rateSourceCryptoVolume, 2, BigDecimal.ROUND_UP);
+ return result.divide(rateSourceCryptoVolume, 2, RoundingMode.UP);
}
return null;
}
@@ -168,7 +152,7 @@ public BigDecimal getExchangeRateForSell(String cryptoCurrency, String fiatCurre
BigDecimal rateSourceCryptoVolume = getRateSourceCryptoVolume(cryptoCurrency);
BigDecimal result = calculateSellPrice(cryptoCurrency, fiatCurrency, rateSourceCryptoVolume);
if (result != null) {
- return result.divide(rateSourceCryptoVolume, 2, BigDecimal.ROUND_DOWN);
+ return result.divide(rateSourceCryptoVolume, 2, RoundingMode.DOWN);
}
return null;
}
@@ -187,7 +171,16 @@ private BigDecimal calculatePrice(String cryptoCurrency, String fiatCurrency, Bi
if (!isCryptoCurrencySupported(cryptoCurrency) || !isFiatCurrencySupported(fiatCurrency)) {
return null;
}
- return call("calculate " + orderSide + " price", () -> api.quoteOrder(new QuoteRequest(cryptoAmount, getMarketSymbol(cryptoCurrency, fiatCurrency), orderSide, OrderType.LIMIT)).fillPrice.multiply(cryptoAmount));
+ String marketSymbol = getMarketSymbol(cryptoCurrency, fiatCurrency);
+ OrderBook orderBook = call(marketSymbol + " orderBook", () -> api.getOrderBook(marketSymbol, orderBookDepth).data);
+ if (orderBook == null) {
+ log.error("failed to get order book");
+ return null;
+ }
+ if (orderSide == OrderSide.SELL) {
+ return orderBookPriceCalculator.getSellPrice(cryptoAmount, orderBook.bids);
+ }
+ return orderBookPriceCalculator.getBuyPrice(cryptoAmount, orderBook.asks);
}
class OrderTask implements ITask {
@@ -196,7 +189,7 @@ class OrderTask implements ITask {
private final long checkTillTime;
private final BigDecimal cryptoAmount;
private final String cryptoCurrency;
- private final String fiatCurrencyToUse;
+ private final String fiatCurrency;
private final OrderSide orderSide;
private String orderId;
@@ -206,24 +199,19 @@ class OrderTask implements ITask {
OrderTask(BigDecimal cryptoAmount, String cryptoCurrency, String fiatCurrencyToUse, OrderSide orderSide) {
this.cryptoAmount = cryptoAmount;
this.cryptoCurrency = cryptoCurrency;
- this.fiatCurrencyToUse = fiatCurrencyToUse;
+ fiatCurrency = fiatCurrencyToUse;
this.orderSide = orderSide;
this.checkTillTime = System.currentTimeMillis() + MAXIMUM_TIME_TO_WAIT_FOR_ORDER_TO_FINISH;
}
@Override
public boolean onCreate() {
- log.info("Calling exchange ({} {} {})", orderSide, cryptoAmount, cryptoCurrency);
- orderId = call("task submitLimitOrder", () -> {
- QuoteRequest quoteRequest = new QuoteRequest(cryptoAmount, getMarketSymbol(cryptoCurrency, fiatCurrencyToUse), orderSide, OrderType.LIMIT);
- QuoteResponse quote = api.quoteOrder(quoteRequest);
-
- OrderRequest orderRequest = new OrderRequest(cryptoAmount, quote.fillPrice, getMarketSymbol(cryptoCurrency, fiatCurrencyToUse), orderSide, OrderType.LIMIT);
- log.info("Submitting order: {}", orderRequest);
- OrderResponse order = api.submitOrder(orderRequest);
- return order.id;
- });
- return (orderId != null);
+ log.info("Calling exchange ({} {} {}-{})", orderSide, cryptoAmount, cryptoCurrency, fiatCurrency);
+
+ QuoteRequest request = new QuoteRequest(orderSide, cryptoCurrency, fiatCurrency, cryptoAmount);
+ orderId = call("task createOrder", () -> api.createOrder(request).data.rfqResponse.id);
+ call("task executeOrder", () -> api.executeOrder(orderId));
+ return orderId != null;
}
@Override
@@ -240,19 +228,12 @@ public boolean onDoStep() {
return false;
}
- OrderResponse order = call("task getOrder", () -> api.getOrder(getMarketSymbol(cryptoCurrency, fiatCurrencyToUse), orderId));
-
- if (order != null && order.status.equals(OrderResponse.STATUS_CANCELLED)) {
- log.debug("trade cancelled");
- finished = true;
- return false;
- }
- if (order != null && order.status.equals(OrderResponse.STATUS_FILLED)) {
- result = orderId;
- finished = true;
- }
+ // we assume the order was executed already when we called api.executeOrder()
+ // we're not able to query the order status from the API
- return result != null;
+ result = orderId;
+ finished = true;
+ return true;
}
@Override
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyMacData.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyMacData.java
deleted file mode 100644
index 0f1687f10..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyMacData.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy;
-
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonPropertyOrder;
-import si.mazi.rescu.RestInvocation;
-
-// THE ORDER OF THE FIELDS IS IMPORTANT!
-// The bitbuy documentation uses JSONObject which uses HashMap where elements are unordered
-// but the MAC is computed over the resulting JSON string
-@JsonPropertyOrder({"path", "content-length", "query"})
-class BitbuyMacData {
- @JsonProperty("path")
- public String path;
- @JsonProperty("content-length")
- public int contentLength;
- @JsonProperty("query")
- public String query;
-
- public static BitbuyMacData from(RestInvocation restInvocation) {
- BitbuyMacData macData = new BitbuyMacData();
- macData.path = restInvocation.getPath();
- macData.contentLength = getBodyLength(restInvocation.getRequestBody());
- macData.query = restInvocation.getQueryString();
- return macData;
- }
-
- private static int getBodyLength(String requestBody) {
- return (requestBody == null || requestBody.isEmpty()) ? -1 : requestBody.length();
- }
-}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/IBitbuyAPI.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/IBitbuyAPI.java
index 55af97b74..615121ef6 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/IBitbuyAPI.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/IBitbuyAPI.java
@@ -17,24 +17,21 @@
************************************************************************************/
package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.Coin;
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.Balance;
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.BitbuyResponse;
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.CreateOrderResponse;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.DepositAddress;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.Market;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderResponse;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderRequest;
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.OrderBook;
import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.QuoteRequest;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.QuoteResponse;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.Wallet;
-import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto.WithdrawResult;
+import com.generalbytes.batm.server.extensions.util.OrderBookPriceCalculator;
import com.generalbytes.batm.server.extensions.util.net.RateLimitingInterceptor;
-import org.knowm.xchange.utils.nonce.CurrentTimeIncrementalNonceFactory;
import si.mazi.rescu.ClientConfig;
+import si.mazi.rescu.ClientConfigUtil;
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;
@@ -42,62 +39,48 @@
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
-import java.math.BigDecimal;
-import java.security.GeneralSecurityException;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-@Path("/api/v1")
+@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public interface IBitbuyAPI {
+ String prodUrl = "https://api-crypto.bitbuy.ca";
+ String testUrl = "https://bb-api-crypto-qa.blockchainmarkets.com";
- static IBitbuyAPI create(String apiKey, String apiSecret) throws GeneralSecurityException {
+ static IBitbuyAPI create(String clientId, String secretKey) {
final ClientConfig config = new ClientConfig();
- config.addDefaultParam(QueryParam.class, "apikey", apiKey);
- config.addDefaultParam(QueryParam.class, "stamp", new CurrentTimeIncrementalNonceFactory(TimeUnit.MILLISECONDS));
- config.addDefaultParam(HeaderParam.class, "signature", new BitbuyDigest(apiSecret));
+ ClientConfigUtil.addBasicAuthCredentials(config, clientId, secretKey);
Interceptor interceptor = new RateLimitingInterceptor(IBitbuyAPI.class, 25, 30_000);
- return RestProxyFactory.createProxy(IBitbuyAPI.class, "https://partner.bcm.exchange", config, interceptor);
+ return RestProxyFactory.createProxy(IBitbuyAPI.class, prodUrl, config, interceptor);
}
-
- @GET
- @Path("/wallets")
- List getWallets() throws IOException;
-
- @GET
- @Path("/coins")
- List getCoins() throws IOException;
-
+ /**
+ * @param currency fiat or crypto currency
+ */
@GET
- @Path("/markets")
- List getMarkets() throws IOException;
+ @Path("/account/balance")
+ BitbuyResponse getBalance(@QueryParam("currency") String currency) throws IOException;
/**
* @return deposit address for the given coin (new address is NOT generated on each call)
*/
@GET
- @Path("/wallets/{coin}/deposit-address")
- DepositAddress getDepositAddress(@PathParam("coin") String coin) throws IOException;
-
- @POST
- @Path("/wallets/{coin}/withdraw")
- WithdrawResult withdraw(@PathParam("coin") String coin, @QueryParam("address") String address, @QueryParam("amount") BigDecimal amount) throws IOException;
-
+ @Path("/account/deposit-address")
+ BitbuyResponse getDepositAddress(@QueryParam("coin") String coin) throws IOException;
@POST
@Consumes(MediaType.APPLICATION_JSON)
- @Path("/submit/order")
- OrderResponse submitOrder(OrderRequest orderRequest) throws IOException;
-
- @GET
- @Path("/single-order")
- OrderResponse getOrder(@QueryParam("marketSymbol") String marketSymbol, @QueryParam("orderId") String orderId) throws IOException;
-
+ @Path("/rfq/create")
+ BitbuyResponse createOrder(QuoteRequest quoteRequest) throws IOException;
@POST
- @Consumes(MediaType.APPLICATION_JSON)
- @Path("/quote/trade")
- QuoteResponse quoteOrder(QuoteRequest quoteRequest) throws IOException;
+ @Path("/rfq/execute")
+ BitbuyResponse executeOrder(@QueryParam("id") String orderId) throws IOException;
+ /**
+ * @param depth number of bids and asks, e.g. for depth 10, 10 asks + 10 bids will be returned.
+ * Increase this if {@link OrderBookPriceCalculator} does not have enough orders to compute the price.
+ */
+ @GET
+ @Path("/public/markets/{marketId}/{depth}")
+ BitbuyResponse getOrderBook(@PathParam("marketId") String market, @PathParam("depth") int depth) throws IOException;
}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Balance.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Balance.java
new file mode 100644
index 000000000..aa91ab2e6
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Balance.java
@@ -0,0 +1,9 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+import java.math.BigDecimal;
+
+public class Balance {
+ public String symbol;
+ public BigDecimal reserved;
+ public BigDecimal available;
+}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/BitbuyResponse.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/BitbuyResponse.java
new file mode 100644
index 000000000..b691ea63a
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/BitbuyResponse.java
@@ -0,0 +1,8 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+public class BitbuyResponse {
+ public T data;
+ public String status;
+ public String message;
+ public Integer statusCode;
+}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Coin.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Coin.java
deleted file mode 100644
index 871ec6fce..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Coin.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
-
-public class Coin {
- public String symbol;
- public String name;
-
- @Override
- public String toString() {
- return name + '(' + symbol + ')';
- }
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CreateOrderResponse.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CreateOrderResponse.java
new file mode 100644
index 000000000..5777067fb
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CreateOrderResponse.java
@@ -0,0 +1,6 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+public class CreateOrderResponse {
+ public QuoteRequest rfqRequest;
+ public QuoteResponse rfqResponse;
+}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CurrencySide.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CurrencySide.java
new file mode 100644
index 000000000..843b6b703
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/CurrencySide.java
@@ -0,0 +1,6 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+public enum CurrencySide {
+ BASE,
+ QUOTE
+}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/DepositAddress.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/DepositAddress.java
index 6ea6c1457..653decba8 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/DepositAddress.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/DepositAddress.java
@@ -2,9 +2,4 @@
public class DepositAddress {
public String address;
-
- @Override
- public String toString() {
- return address;
- }
}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Market.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Market.java
deleted file mode 100644
index a73d3bdbe..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Market.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
-
-public class Market {
- public String symbol;
- public String baseCoin;
- public String tradeCoin;
-
- @Override
- public String toString() {
- return symbol;
- }
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBook.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBook.java
new file mode 100644
index 000000000..adb24f67c
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBook.java
@@ -0,0 +1,9 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+import java.util.List;
+
+public class OrderBook {
+ public String marketSymbol;
+ public List asks;
+ public List bids;
+}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBookLevel.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBookLevel.java
new file mode 100644
index 000000000..f7a1159cb
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderBookLevel.java
@@ -0,0 +1,9 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+import java.math.BigDecimal;
+
+public class OrderBookLevel {
+ public BigDecimal pricePerUnit;
+ public BigDecimal quantity;
+ public BigDecimal total;
+}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderRequest.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderRequest.java
deleted file mode 100644
index dd00b68f7..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderRequest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
-
-import java.math.BigDecimal;
-
-public class OrderRequest {
- public BigDecimal quantity;
- public BigDecimal pricePerUnit;
- public String marketSymbol;
- public OrderSide orderSide;
- public OrderType orderType;
-
- public OrderRequest() {
- }
-
- public OrderRequest(BigDecimal quantity, BigDecimal pricePerUnit, String marketSymbol, OrderSide orderSide, OrderType orderType) {
- this.quantity = quantity;
- this.pricePerUnit = pricePerUnit;
- this.marketSymbol = marketSymbol;
- this.orderSide = orderSide;
- this.orderType = orderType;
- }
-
- @Override
- public String toString() {
- return "OrderRequest{" +
- "quantity=" + quantity +
- ", pricePerUnit=" + pricePerUnit +
- ", marketSymbol='" + marketSymbol + '\'' +
- ", orderSide=" + orderSide +
- ", orderType=" + orderType +
- '}';
- }
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderResponse.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderResponse.java
deleted file mode 100644
index f78022796..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderResponse.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
-
-import java.math.BigDecimal;
-
-public class OrderResponse {
- // not an enum because all possible values not documented
- public static final String STATUS_CANCELLED = "CANCELLED";
- public static final String STATUS_PENDING_NEW = "PENDING_NEW";
- public static final String STATUS_FILLED = "FILLED";
-
- public String id;
- public String status;
- public String statusInfo;
- public BigDecimal fillPrice;
-
- @Override
- public String toString() {
- return "OrderResponse{" +
- "id='" + id + '\'' +
- ", status='" + status + '\'' +
- ", statusInfo='" + statusInfo + '\'' +
- ", fillPrice=" + fillPrice +
- '}';
- }
-
- /*
- Response examples:
- - PENDING_NEW: {"id":"7782220156104123330","side":"BUY","date":"2021-10-04 14:54:34","pricePerUnit":"220.08000000","quantity":"0.01000000", "totalValue":"2.20080000", "market":{"tradeCoin":"LTC","baseCoin":"CAD","symbol":"LTC-CAD","state":null},"status":"PENDING_NEW","leavesQuantity":"0.00000000","fillCount":"0","tradedQuantity":"0.00000000","cancelledQuantity":"0.00000000","statusInfo":null, "totalValueTraded":null,"weightedAverageFillPrice":null,"cumulativeFee":null,"type":"MARKET"}
- - FILLED: {"id":"7782220156104417848","side":"SELL","date":"2021-10-05 14:20:36","pricePerUnit":"1.35700000","quantity":"0.00009980", "totalValue":"0.00013542", "market":{"tradeCoin":"XRP","baseCoin":"CAD","symbol":"XRP-CAD","state":null},"status":"FILLED", "leavesQuantity":"0.00000000","fillCount":"1","tradedQuantity":"0.00009980","cancelledQuantity":"0.00000000","statusInfo":null, "totalValueTraded":"0.00013543","weightedAverageFillPrice":"1.35700000","cumulativeFee":"0.00000027","type":"LIMIT"}
- - CANCELLED (insufficient funds): {"id":"0", "side":"BUY","date":"2021-10-04 14:59:21","pricePerUnit":"218.41050000","quantity":"1000.00000000","totalValue":"218410.50000000","market":{"tradeCoin":"LTC","baseCoin":"CAD","symbol":"LTC-CAD","state":null},"status":"CANCELLED", "leavesQuantity":"0.00000000","fillCount":"0","tradedQuantity":"0.00000000","cancelledQuantity":"0.00000000","statusInfo":"Insufficient Funds","totalValueTraded":null,"weightedAverageFillPrice":null,"cumulativeFee":null,"type":"MARKET"}
- */
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderStatus.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderStatus.java
new file mode 100644
index 000000000..7b5abbd34
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/OrderStatus.java
@@ -0,0 +1,8 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+public enum OrderStatus {
+ CANCELLED,
+ FULFILLED,
+ EXPIRED,
+ OPEN
+}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Paginated.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Paginated.java
new file mode 100644
index 000000000..738f18487
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Paginated.java
@@ -0,0 +1,14 @@
+package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
+
+import java.util.List;
+
+public class Paginated {
+ public List result;
+
+ public Integer pageNumber;
+ public Integer totalPages;
+ public Integer currentCount;
+ public Integer totalCount;
+ public Integer limit;
+ public Boolean hasNextPage;
+}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteRequest.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteRequest.java
index 0a63d2b1d..266a26ac2 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteRequest.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteRequest.java
@@ -3,28 +3,20 @@
import java.math.BigDecimal;
public class QuoteRequest {
- public BigDecimal quantity;
- public String marketSymbol;
- public OrderSide orderSide;
- public OrderType orderType;
+ public CurrencySide currencySide;
+ public String quantity;
+ public OrderSide side;
+ public String quote; // CAD
+ public String base; // crypto
public QuoteRequest() {
}
- public QuoteRequest(BigDecimal quantity, String marketSymbol, OrderSide orderSide, OrderType orderType) {
- this.quantity = quantity;
- this.marketSymbol = marketSymbol;
- this.orderSide = orderSide;
- this.orderType = orderType;
- }
-
- @Override
- public String toString() {
- return "OrderRequest{" +
- "quantity=" + quantity +
- ", marketSymbol='" + marketSymbol + '\'' +
- ", orderSide=" + orderSide +
- ", orderType=" + orderType +
- '}';
+ public QuoteRequest(OrderSide orderSide, String cryptoCurrency, String fiatCurrency, BigDecimal cryptoAmount) {
+ this.currencySide = CurrencySide.BASE;
+ this.quantity = cryptoAmount.toPlainString();
+ this.side = orderSide;
+ this.quote = fiatCurrency;
+ this.base = cryptoCurrency;
}
}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteResponse.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteResponse.java
index 5db5f25ac..ce90a8197 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteResponse.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/QuoteResponse.java
@@ -3,16 +3,13 @@
import java.math.BigDecimal;
public class QuoteResponse {
- public BigDecimal fillPrice;
-
- @Override
- public String toString() {
- return "QuoteResponse{" +
- "fillPrice=" + fillPrice +
- '}';
- }
- /*
- Response example:
- {"originalQuantity":"1.00000000","originalQuantityCoinSymbol":"BTC","market":{"tradeCoin":"BTC","baseCoin":"CAD","symbol":"BTC-CAD","state":null},"baseCoinFillQuantity":"63002.71231682","tradeCoinFillQuantity":"1.00000000","fillPrice":"63097.00000000","averageFillprice":"63002.71231682","side":"BUY","status":null,"statusInfo":null}
- */
+ public String id;
+ public Long createdAt;
+ public Long expireAt;
+ public OrderSide side;
+ public String base; // crypto currency
+ public BigDecimal baseQuantity;
+ public String quote; // CAD
+ public BigDecimal quoteQuantity;
+ public BigDecimal unitPrice;
}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Wallet.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Wallet.java
deleted file mode 100644
index 69abe62a7..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/Wallet.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
-
-import java.math.BigDecimal;
-
-public class Wallet {
- public String symbol;
- public BigDecimal balance;
- public BigDecimal reservedBalance;
- public BigDecimal availableBalance;
-
- @Override
- public String toString() {
- return "Wallet{" +
- "symbol='" + symbol + '\'' +
- ", balance='" + balance + '\'' +
- ", reservedBalance='" + reservedBalance + '\'' +
- ", availableBalance='" + availableBalance + '\'' +
- '}';
- }
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/WithdrawResult.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/WithdrawResult.java
deleted file mode 100644
index e3e04a458..000000000
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/dto/WithdrawResult.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy.dto;
-
-public class WithdrawResult {
- public String transactionReference;
-
- @Override
- public String toString() {
- return transactionReference;
- }
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculator.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculator.java
new file mode 100644
index 000000000..baed77459
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculator.java
@@ -0,0 +1,73 @@
+package com.generalbytes.batm.server.extensions.util;
+
+import java.math.BigDecimal;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Calculates buy or sell price from order book orders (bids or asks) downloaded from an exchange.
+ * The price depends on the amount you want to buy or sell,
+ * e.g. the price of one BTC when buying 10 BTC might be higher than when buying just 1 BTC.
+ *
+ * @param Type of the order book order objects used to calculate the price.
+ * It must be possible to obtain price and amount of each order.
+ */
+public class OrderBookPriceCalculator {
+ private final Comparator asksComparator;
+ private final Comparator bidsComparator;
+ private final Function orderLimitPriceGetter;
+ private final Function orderAmountGetter;
+
+ /**
+ * @param orderLimitPriceGetter a function that retrieves limit price (price per unit) of the order in the order book
+ * @param orderAmountGetter a function that retrieves cryptocurrency amount (quantity) of the order in the order book
+ */
+ public OrderBookPriceCalculator(Function orderLimitPriceGetter, Function orderAmountGetter) {
+ // bids: highest price first (used for sell)
+ // asks: lowest price first (used for buy)
+ this.asksComparator = Comparator.comparing(orderLimitPriceGetter);
+ this.bidsComparator = Comparator.comparing(orderLimitPriceGetter).reversed();
+ this.orderLimitPriceGetter = orderLimitPriceGetter;
+ this.orderAmountGetter = orderAmountGetter;
+ }
+
+ /**
+ * Calculates the price (per one unit of cryptocurrency) we would get if we wanted to SELL the given amount.
+ *
+ * @param bids bids from the order book.
+ * Bids are other people's BUY orders (we are selling, others are buying).
+ * Bids prices are lower than the last traded price (the midpoint price).
+ * We use the bids with the highest prices first until we reach the required amount.
+ */
+ public BigDecimal getSellPrice(BigDecimal cryptoAmount, List bids) {
+ return getPrice(cryptoAmount, bids, bidsComparator);
+ }
+
+ /**
+ * Calculates the price (per one unit of cryptocurrency) we would have to pay if we wanted to BUY the given amount.
+ *
+ * @param asks asks from the order book.
+ * Asks are other people's SELL orders (we are buying, others are selling).
+ * Asks prices are higher than the last traded price (the midpoint price).
+ * We use the asks with the lowest prices first until we reach the required amount.
+ */
+ public BigDecimal getBuyPrice(BigDecimal cryptoAmount, List asks) {
+ return getPrice(cryptoAmount, asks, asksComparator);
+ }
+
+ private BigDecimal getPrice(BigDecimal cryptoAmount, List orders, Comparator comparator) {
+ Objects.requireNonNull(orders, "orders list cannot be null");
+ List sorted = orders.stream().sorted(comparator).collect(Collectors.toList());
+ BigDecimal total = BigDecimal.ZERO;
+ for (T order : sorted) {
+ total = total.add(orderAmountGetter.apply(order));
+ if (cryptoAmount.compareTo(total) <= 0) {
+ return orderLimitPriceGetter.apply(order);
+ }
+ }
+ throw new IllegalArgumentException("tradable price not available");
+ }
+}
diff --git a/server_extensions_extra/src/main/resources/batm-extensions.xml b/server_extensions_extra/src/main/resources/batm-extensions.xml
index a2a01022e..bc98c36f3 100644
--- a/server_extensions_extra/src/main/resources/batm-extensions.xml
+++ b/server_extensions_extra/src/main/resources/batm-extensions.xml
@@ -806,24 +806,18 @@
-
BCH
BTC
- DAI
ETH
LTC
- XRP
- fiatcurrency is optional, default: CAD
BCH
BTC
- DAI
ETH
LTC
- XRP
diff --git a/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigestTest.java b/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigestTest.java
deleted file mode 100644
index 16988a464..000000000
--- a/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyDigestTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.bitbuy;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.junit.Assert;
-import org.junit.Test;
-import si.mazi.rescu.RequestWriterResolver;
-import si.mazi.rescu.RestInvocation;
-import si.mazi.rescu.RestMethodMetadata;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-
-public class BitbuyDigestTest {
-
- private interface TestApi {
- @GET
- @Path("/test/path")
- String testGet(@QueryParam("param1") String param1, @QueryParam("param2") String param2) throws IOException;
-
- @POST
- @Path("/test/path")
- @Consumes(MediaType.APPLICATION_JSON)
- String testPost(@QueryParam("param1") String param1, @QueryParam("param2") String param2, String body) throws IOException;
- }
-
- @Test
- public void testGetMacDataGet() throws GeneralSecurityException, NoSuchMethodException {
- Method method = TestApi.class.getMethod("testGet", String.class, String.class);
- String expected = "{\"path\":\"interfacepath/test/path\",\"content-length\":-1,\"query\":\"param1=param1value¶m2=param2value\"}";
- Assert.assertEquals(expected, getMacData(method, "param1value", "param2value"));
- }
-
- @Test
- public void testGetMacDataPost() throws GeneralSecurityException, NoSuchMethodException {
- Method method = TestApi.class.getMethod("testPost", String.class, String.class, String.class);
- String expected = "{\"path\":\"interfacepath/test/path\",\"content-length\":6,\"query\":\"param1=param1value¶m2=param2value\"}";
- Assert.assertEquals(expected, getMacData(method, "param1value", "param2value", "body"));
- }
-
- private String getMacData(Method method, Object... args) throws GeneralSecurityException {
- RestMethodMetadata methodMetadata = RestMethodMetadata.create(method, "https://baseurl/", "interfacepath");
- RequestWriterResolver requestWriterResolver = RequestWriterResolver.createDefault(new ObjectMapper());
- RestInvocation restInvocation = RestInvocation.create(requestWriterResolver, methodMetadata, args, null);
-
- BitbuyDigest digest = new BitbuyDigest("secret");
- return new String(digest.getMacData(restInvocation), StandardCharsets.UTF_8);
- }
-}
\ No newline at end of file
diff --git a/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculatorTest.java b/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculatorTest.java
new file mode 100644
index 000000000..afa2d33ec
--- /dev/null
+++ b/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/util/OrderBookPriceCalculatorTest.java
@@ -0,0 +1,59 @@
+package com.generalbytes.batm.server.extensions.util;
+
+import org.assertj.core.api.Assertions;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+public class OrderBookPriceCalculatorTest {
+
+ private static class Order {
+ public final BigDecimal price;
+ public final BigDecimal amount;
+
+ private Order(int price, int amount) {
+ this.price = new BigDecimal(price);
+ this.amount = new BigDecimal(amount);
+ }
+ }
+ private static final OrderBookPriceCalculator calc = new OrderBookPriceCalculator<>(order -> order.price, order -> order.amount);
+
+ @Test
+ public void test() throws IOException {
+
+ List asks = new ArrayList<>();
+ List bids = new ArrayList<>();
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> calc.getBuyPrice(BigDecimal.ONE, asks));
+ Assert.assertThrows(IllegalArgumentException.class, () -> calc.getSellPrice(BigDecimal.ONE, bids));
+
+ bids.add(new Order(50, 2));
+ bids.add(new Order(70, 2));
+ bids.add(new Order(90, 2));
+
+ asks.add(new Order(110, 2));
+ asks.add(new Order(130, 2));
+ asks.add(new Order(150, 2));
+
+ Assertions.assertThat(calc.getBuyPrice(new BigDecimal("0.001"), asks)).isEqualByComparingTo("110");
+ Assertions.assertThat(calc.getSellPrice(new BigDecimal("0.001"), bids)).isEqualByComparingTo("90");
+
+ Assertions.assertThat(calc.getBuyPrice(new BigDecimal("3"), asks)).isEqualByComparingTo("130");
+ Assertions.assertThat(calc.getSellPrice(new BigDecimal("3"), bids)).isEqualByComparingTo("70");
+
+ Assertions.assertThat(calc.getBuyPrice(new BigDecimal("5"), asks)).isEqualByComparingTo("150");
+ Assertions.assertThat(calc.getSellPrice(new BigDecimal("5"), bids)).isEqualByComparingTo("50");
+
+ Assertions.assertThat(calc.getBuyPrice(new BigDecimal("6"), asks)).isEqualByComparingTo("150");
+ Assertions.assertThat(calc.getSellPrice(new BigDecimal("6"), bids)).isEqualByComparingTo("50");
+
+ Assert.assertThrows(IllegalArgumentException.class, () -> calc.getBuyPrice(new BigDecimal("6.1"), asks));
+ Assert.assertThrows(IllegalArgumentException.class, () -> calc.getSellPrice(new BigDecimal("6.1"), bids));
+
+ }
+
+}
\ No newline at end of file
From 785e621e4bbba951d51b6cb4cf20ccb69e98c4fe Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Tue, 25 Jul 2023 11:00:40 +0200
Subject: [PATCH 02/82] BATM-5238 Log currency pair at XChangeExchange when
asking for limits (#830)
Co-authored-by: Filip Ocelka
---
gradle.properties | 2 +-
.../extensions/extra/bitcoin/exchanges/XChangeExchange.java | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index 2f8a46cdd..646a7a892 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.8
+projectVersion=1.1.9
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java
index 40578a1ba..65f5b252f 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/XChangeExchange.java
@@ -554,7 +554,8 @@ public BigDecimal calculateSellPrice(String cryptoCurrency, String fiatCurrency,
}
if (tradableLimit == null) {
- log.error("Not enough bids received from the exchange, bids count: {}, bids total: {}, crypto amount: {}", bids.size(), bidsTotal, cryptoAmount);
+ log.error("Not enough bids received from the exchange, bids count: {}, bids total: {}, crypto amount: {}, currency pair: {}",
+ bids.size(), bidsTotal, cryptoAmount, currencyPair);
return null;
}
log.debug("Called {} exchange for SELL rate: {}:{} = {}", name, cryptoCurrency, fiatCurrency, tradableLimit);
From 420a885cb4ad2bef6e9e79fe84c8a88384628f3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Tue, 8 Aug 2023 13:33:01 +0200
Subject: [PATCH 03/82] BATM-5263 - Coinbase Hot Wallet API change at response
object (#834)
---
gradle.properties | 2 +-
.../coinbase/v2/dto/CBAccountResponse.java | 6 +-
.../wallets/coinbase/v2/dto/CbAccountV2.java | 155 ++++++++++++++++++
3 files changed, 159 insertions(+), 4 deletions(-)
create mode 100644 server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CbAccountV2.java
diff --git a/gradle.properties b/gradle.properties
index 3a1282e69..5c657e021 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.3.1
+projectVersion=1.1.3.2
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CBAccountResponse.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CBAccountResponse.java
index e53e0bd92..f51b7cff8 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CBAccountResponse.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CBAccountResponse.java
@@ -22,14 +22,14 @@
*/
public class CBAccountResponse extends CBResponse{
- private CBAccount data;
+ private CbAccountV2 data;
- public CBAccount getData() {
+ public CbAccountV2 getData() {
return data;
}
- public void setData(CBAccount data) {
+ public void setData(CbAccountV2 data) {
this.data = data;
}
}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CbAccountV2.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CbAccountV2.java
new file mode 100644
index 000000000..365d8d543
--- /dev/null
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/wallets/coinbase/v2/dto/CbAccountV2.java
@@ -0,0 +1,155 @@
+/*************************************************************************************
+ * Copyright (C) 2014-2020 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.wallets.coinbase.v2.dto;
+
+import com.generalbytes.batm.server.extensions.extra.bitcoin.exchanges.coinbase.dto.CBCurrency;
+
+/**
+ * Created by b00lean on 23.7.17.
+ */
+
+public class CbAccountV2 implements CBPaginatedItem {
+ private String id;
+ private String address;
+ private String name;
+ private boolean primary;
+ private boolean ready;
+ private String type;
+ private CBCurrency currency;
+ private CBBalance balance;
+ private CBBalance native_balance;
+
+ private String created_at;
+ private String updated_at;
+ private String network;
+ private String resource;
+ private String resource_path;
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getCreated_at() {
+ return created_at;
+ }
+
+ public void setCreated_at(String created_at) {
+ this.created_at = created_at;
+ }
+
+ public String getUpdated_at() {
+ return updated_at;
+ }
+
+ public void setUpdated_at(String updated_at) {
+ this.updated_at = updated_at;
+ }
+
+ public String getNetwork() {
+ return network;
+ }
+
+ public void setNetwork(String network) {
+ this.network = network;
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getResource_path() {
+ return resource_path;
+ }
+
+ public void setResource_path(String resource_path) {
+ this.resource_path = resource_path;
+ }
+
+ public boolean isPrimary() {
+ return primary;
+ }
+
+ public void setPrimary(boolean primary) {
+ this.primary = primary;
+ }
+
+ public boolean isReady() {
+ return ready;
+ }
+
+ public void setReady(boolean ready) {
+ this.ready = ready;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public CBCurrency getCurrency() {
+ return currency;
+ }
+
+ public void setCurrency(CBCurrency currency) {
+ this.currency = currency;
+ }
+
+ public CBBalance getBalance() {
+ return balance;
+ }
+
+ public void setBalance(CBBalance balance) {
+ this.balance = balance;
+ }
+
+ public CBBalance getNative_balance() {
+ return native_balance;
+ }
+
+ public void setNative_balance(CBBalance native_balance) {
+ this.native_balance = native_balance;
+ }
+}
From 6e23a904560f5c507545362a42804efea2fdc91c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Mon, 14 Aug 2023 14:35:07 +0200
Subject: [PATCH 04/82] BATM-5283 - Incorrect XRP conversion (#837)
---
.../com/generalbytes/batm/server/extensions/Converters.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/Converters.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/Converters.java
index 0cb5e9267..d4d0fd174 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/Converters.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/Converters.java
@@ -29,7 +29,7 @@ public class Converters {
public static final BigDecimal GQ = BigDecimal.TEN.pow(18);
public static final BigDecimal USDT = BigDecimal.TEN.pow(6);
public static final BigDecimal USDTTRON = BigDecimal.TEN.pow(6);
- public static final BigDecimal XRP = BigDecimal.TEN.pow(8);
+ public static final BigDecimal XRP = BigDecimal.TEN.pow(6);
public static final BigDecimal VERUM = BigDecimal.TEN.pow(8);
public static final BigDecimal TBCH = BigDecimal.TEN.pow(8);
From 12ab6f40de1dcc97f63d7dbde44daeabe0134fd6 Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Mon, 14 Aug 2023 14:39:09 +0200
Subject: [PATCH 05/82] Increased project version
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index d96a06517..6823689fc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.10
+projectVersion=1.1.11
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
From a82511798c4f31444b35ccd0b08fd9f6607b5caf Mon Sep 17 00:00:00 2001
From: Premek
Date: Tue, 15 Aug 2023 08:04:33 +0200
Subject: [PATCH 06/82] LC-169 Add ability to modify
transactionrecordcustomdata from extensions (#831)
---
gradle.properties | 2 +-
.../batm/server/extensions/IExtensionContext.java | 14 ++++++++++++++
.../server/extensions/ITransactionDetails.java | 9 +++++++++
.../server/extensions/TestExtensionContext.java | 5 +++++
.../extra/examples/TransactionExtension.java | 11 ++++++++++-
5 files changed, 39 insertions(+), 2 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index 6823689fc..87a04243c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.11
+projectVersion=1.1.12
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
index 0db56c2a4..9ba3a8294 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
@@ -131,6 +131,20 @@ public interface IExtensionContext {
*/
ITransactionDetails updateTransaction(String rid, Integer status, String detail, Set tags) throws UpdateException;
+ /**
+ * @param rid remote transaction ID of the transaction to be updated
+ * @param status new status to be set or null to keep it unmodified
+ * @param detail detail message to be appended if there already is a detail set. Null to keep it unmodified
+ * @param customData custom data to be set to the transaction.
+ * This will replace existing custom data stored for the transaction.
+ * If you need to keep existing data obtain them first using {@link ITransactionDetails#getCustomData()}.
+ * Providing an empty map will remove all existing custom data.
+ * Null keeps the existing custom data unchanged.
+ * @return modified transaction details
+ * @throws UpdateException if the update was not successful
+ */
+ ITransactionDetails updateTransaction(String rid, Integer status, String detail, Map customData) throws UpdateException;
+
ITransactionDetails updateTransaction(String rid, Integer status, String detail) throws UpdateException;
/**
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java
index 53c92038b..5524e3a6f 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java
@@ -20,6 +20,7 @@
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Set;
public interface ITransactionDetails {
@@ -303,4 +304,12 @@ public interface ITransactionDetails {
Set getTags();
List getBanknotes();
+
+ /**
+ * @return Custom data for the transaction.
+ * Saved when returned from {@link ITransactionListener#onTransactionCreated(ITransactionDetails)}
+ * or {@link ITransactionListener#onTransactionUpdated(ITransactionDetails)}
+ * or using {@link IExtensionContext#updateTransaction(String, Integer, String, Map)}.
+ */
+ Map getCustomData();
}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
index 0269544d1..aa6458f50 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
@@ -87,6 +87,11 @@ public ITransactionDetails updateTransaction(String rid, Integer status, String
return null;
}
+ @Override
+ public ITransactionDetails updateTransaction(String rid, Integer status, String detail, Map customData) throws UpdateException {
+ return null;
+ }
+
@Override
public Set getTransactionTags(String organizationId) {
return null;
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java
index 2ba8957a0..a8921d20f 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java
@@ -61,10 +61,19 @@ public Map onTransactionCreated(ITransactionDetails transactionD
public Map onTransactionUpdated(ITransactionDetails transactionDetails) {
log.info("Transaction updated; tags: {}", transactionDetails.getTags());
try {
+ String rid = transactionDetails.getRemoteTransactionId();
+
+ ITransactionDetails details = ctx.findTransactionByTransactionId(rid);
+ log.info("Transaction custom data: {}", details.getCustomData());
+ Map customData = new HashMap<>(details.getCustomData());
+ customData.remove("ticket.previous.counter");
+ customData.put("ticket.footer", "Enjoy!");
+ ctx.updateTransaction(rid, null, null, customData);
+
String organizationId = ctx.findIdentityByIdentityId(transactionDetails.getIdentityPublicId()).getOrganization().getId();
log.info("Defined transaction tags: {}", ctx.getTransactionTags(organizationId));
Set tags = Collections.singleton(transactionDetails.getCryptoCurrency());
- ITransactionDetails updated = ctx.updateTransaction(transactionDetails.getRemoteTransactionId(), null, null, tags);
+ ITransactionDetails updated = ctx.updateTransaction(rid, null, null, tags);
log.info("Transaction updated; tags: {}", updated.getTags());
} catch (UpdateException e) {
log.error("", e);
From da4dbdc099dc99e57ffc6e8badd522e7865ea19b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Tue, 15 Aug 2023 08:05:19 +0200
Subject: [PATCH 07/82] CF-484 - Notification - listener method for failed
queued transactions (#832)
---
.../extensions/INotificationListener.java | 29 +++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java
index 1b8d999f9..f522cc81d 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java
@@ -51,6 +51,35 @@ default void transactionFailed(String terminalSerialNumber, BigDecimal cashAmoun
*/
default void transactionQueued(String terminalSerialNumber, BigDecimal cashAmount, String cashCurrency, String transactionRemoteId, String paymentType) {}
+ /**
+ * Invoked when one or more transactions in a queue have failed.
+ *
+ * @param queueName The name of the queue in which the transactions were queued.
+ * @param batchUid A unique identifier representing the batch of transactions.
+ * @param failedTransactions A list of transaction details for those transactions which failed.
+ * Each transaction detail includes information such as the serial number of the terminal.
+ *
+ *
+ * ITransactionDetails will have the following attributes populated:
+ *
+ * - Terminal Serial Number: Sourced from the transaction's terminal..
+ * - Remote ID (Rid): The Rid associated with the transaction
+ * - Local ID (Lid): The Lid associated with the transaction.
+ * - Cash Amount: The fiat amount of transaction.
+ * - Cash Currency: The type of fiat currency used in the transaction (e.g., USD, EUR).
+ * - Crypto Amount: The fiat amount of transaction.
+ * - Crypto Currency: The type of cryptocurrency used in the transaction (e.g., BTC, ETH).
+ * - Crypto Address: The destination address for the cryptocurrency transaction.
+ * - Identity Public ID: Public identity associated with the transaction, if any.
+ *
+ *
+ *
+ * All other attributes are either set to default values or empty.
+ *
+ *
+ */
+ default void queuedTransactionsFailed(String queueName, String batchUid, List failedTransactions) {}
+
default void cashbackCreated(String terminalSerialNumber, BigDecimal cashAmount, String cashCurrency) {}
default void invalidPaymentReceived(String terminalSerialNumber, BigDecimal amount, String cryptoCurrency, String fromAddress, String toAddress, String transactionRemoteId) {}
From 9e844ed5a66baa5af55b86019c9d8483658e2a03 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Tue, 15 Aug 2023 08:06:01 +0200
Subject: [PATCH 08/82] LC-221 - Identity Opt-out and Opt-in endpoint for
setting opt-in/opt-out (#828)
---
.../batm/server/extensions/IExtensionContext.java | 8 ++++++++
.../server/extensions/TestExtensionContext.java | 4 ++++
.../identity/IdentityExampleRestService.java | 14 ++++++++++++++
3 files changed, 26 insertions(+)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
index 9ba3a8294..d11e088bb 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
@@ -323,6 +323,14 @@ IIdentity updateIdentity(String identityId, String externalId, int state, int ty
List limitCashPerMonth, List limitCashPer3Months, List limitCashPer12Months, List limitCashPerCalendarQuarter,
List limitCashPerCalendarYear, List limitCashTotalIdentity, String configurationCashCurrency);
+ /**
+ * Updates the marketing opt-in agreement for the identity identified by {@code identityId}).
+ *
+ * @param identityId public ID of an existing identity to be updated
+ * @param agreeWithMarketingOptIn True if the customer agrees to marketing opt-in, false otherwise.
+ */
+ void updateIdentityMarketingOptIn(String identityId, boolean agreeWithMarketingOptIn);
+
/**
* @param customFieldDefinitionId use {@link CustomFieldDefinition#getId()} of a custom field to set
*/
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
index aa6458f50..f598f4c5e 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
@@ -187,6 +187,10 @@ public IIdentity updateIdentity(String identityId, String externalId, int state,
return null;
}
+ @Override
+ public void updateIdentityMarketingOptIn(String identityId, boolean agreeWithMarketingOptIn) {
+ }
+
@Override
public void setIdentityCustomField(String identityPublicId,
long customFieldDefinitionId,
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/IdentityExampleRestService.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/IdentityExampleRestService.java
index 168134e7d..51e36538b 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/IdentityExampleRestService.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/IdentityExampleRestService.java
@@ -123,6 +123,20 @@ public String update(@FormParam("identityPublicId") String identityPublicId, @Fo
return updatedIdentity.getPublicId();
}
+ // curl -k -XPOST https://localhost:7743/extensions/identity-example/update-opt-in -d "identityPublicId=IE3BVEBUIIXZ3SZV&agreeWithMarketingOptIn=true"
+ @POST
+ @Path("/update-opt-in")
+ @Produces(MediaType.APPLICATION_JSON)
+ public void updateOptInAgreement(@FormParam("identityPublicId") String identityPublicId, @FormParam("agreeWithMarketingOptIn") boolean agreeWithMarketingOptIn) {
+ IExtensionContext ctx = IdentityExampleExtension.getExtensionContext();
+ IIdentity identity = ctx.findIdentityByIdentityId(identityPublicId);
+ if (identity == null) {
+ log.debug("Identity {} not found", identityPublicId);
+ return;
+ }
+ ctx.updateIdentityMarketingOptIn(identityPublicId, agreeWithMarketingOptIn);
+ }
+
// curl -k -XPOST https://localhost:7743/extensions/identity-example/getnotes -d "identityPublicId=IE3BVEBUIIXZ3SZV"
@POST
@Path("/getnotes")
From ba942a1cd81a5f248b110dcff2c946d9fb39c320 Mon Sep 17 00:00:00 2001
From: Sandro
Date: Tue, 15 Aug 2023 13:22:39 +0400
Subject: [PATCH 09/82] - Allow sell transactions for ETH & USDT on CryptX
wallet - Remove GQ & BTBS support
---
.../src/main/resources/batm-extensions.xml | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/server_extensions_extra/src/main/resources/batm-extensions.xml b/server_extensions_extra/src/main/resources/batm-extensions.xml
index bc98c36f3..060894342 100644
--- a/server_extensions_extra/src/main/resources/batm-extensions.xml
+++ b/server_extensions_extra/src/main/resources/batm-extensions.xml
@@ -77,10 +77,8 @@
BCH
BTC
LTC
- BTBS
- GQ
- ETH
- USDT
+ ETH
+ USDT
*host: https://api.cryptx.com/ (required) -
port: (not required) - *token: generate Access tokens from CryptX Settings (required) -
@@ -105,8 +103,8 @@
BCH
BTC
LTC
- ETH
- USDT
+ ETH
+ USDT
*host: https://api.cryptx.com/ (required) -
port: (not required) - *token: generate Access tokens from CryptX Settings (required) -
From b1556b4b350a1c2e4bb16da79c494732c67ed243 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Wed, 13 Sep 2023 12:45:04 +0200
Subject: [PATCH 10/82] LC-163 - Lowercase transaction tags example extension
(#841)
---
.../server/extensions/extra/examples/TransactionExtension.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java
index a8921d20f..c0c91e4c0 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/TransactionExtension.java
@@ -72,7 +72,7 @@ public Map onTransactionUpdated(ITransactionDetails transactionD
String organizationId = ctx.findIdentityByIdentityId(transactionDetails.getIdentityPublicId()).getOrganization().getId();
log.info("Defined transaction tags: {}", ctx.getTransactionTags(organizationId));
- Set tags = Collections.singleton(transactionDetails.getCryptoCurrency());
+ Set tags = Collections.singleton(transactionDetails.getCryptoCurrency().toLowerCase());
ITransactionDetails updated = ctx.updateTransaction(rid, null, null, tags);
log.info("Transaction updated; tags: {}", updated.getTags());
} catch (UpdateException e) {
From 864f2d10f950bf4d33bbc860fcf48fd8fe9e1acf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Wed, 13 Sep 2023 12:45:47 +0200
Subject: [PATCH 11/82] BATM-5339 - CryptX fixed parameters (#845)
---
.../src/main/resources/batm-extensions.xml | 38 +++++++++----------
1 file changed, 18 insertions(+), 20 deletions(-)
diff --git a/server_extensions_extra/src/main/resources/batm-extensions.xml b/server_extensions_extra/src/main/resources/batm-extensions.xml
index 060894342..1d51ae0d7 100644
--- a/server_extensions_extra/src/main/resources/batm-extensions.xml
+++ b/server_extensions_extra/src/main/resources/batm-extensions.xml
@@ -68,25 +68,24 @@
-
+
-
+
-
BCH
BTC
LTC
ETH
USDT
- *host: https://api.cryptx.com/ (required) -
- port: (not required) - *token: generate Access tokens from CryptX Settings (required) -
- *walletid: find under [Wallet] > Settings > Wallet Info > Wallet ID (required) -
- password: wallet password (not required) -
- priority: Select "high" (2 blocks, default), "medium" (8 blocks), "low" (24 blocks), or "custom" -
- max_custom_fee: fee rate (in s/vByte for bitcoin, gas price for ethereum) when "custom" priority is set (instead of estimating one using blocks) -
- limit: gas limit - only for ETH, e.g., 50000 (not required)
+ *host: https://api.walletpro.cryptal.com/ (required) -
+ port: (not required) -
+ *api_key: the API Key obtained from CryptX (required) -
+ *walletid: the name of the wallet on CryptX (required) -
+ passphrase: the wallet passphrase (if used) (not required) -
+ priority: high: (default) = 2 blocks, medium = 8, low = 24, custom - also indicate the number of required blocks (e.g. custom:12) -
+ max_custom_fee: the maximum fee (in s/vByte) when "custom" priority is used
@@ -94,25 +93,24 @@
-
+
-
+
-
BCH
BTC
LTC
ETH
USDT
- *host: https://api.cryptx.com/ (required) -
- port: (not required) - *token: generate Access tokens from CryptX Settings (required) -
- *walletid: find under [Wallet] > Settings > Wallet Info > Wallet ID (required) -
- password: wallet password (not required) -
- priority: Select high (2 blocks, default), medium (8 blocks), low (24 blocks), or custom -
- max_custom_fee: fee rate in s/vByte when "custom" priority is set (instead of estimating one using blocks) -
- limit: gas limit - only for ETH, e.g., 50000 (not required)
+ *host: https://api.walletpro.cryptal.com/ (required) -
+ port: (not required) -
+ *api_key: the API Key obtained from CryptX (required) -
+ *walletid: the name of the wallet on CryptX (required) -
+ passphrase: the wallet passphrase (if used) (not required) -
+ priority: high: (default) = 2 blocks, medium = 8, low = 24, custom - also indicate the number of required blocks (e.g. custom:12) -
+ max_custom_fee: the maximum fee (in s/vByte) when "custom" priority is used
From a629254669371e9db2f9b77a8c190f166cb9a007 Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Wed, 13 Sep 2023 12:47:24 +0200
Subject: [PATCH 12/82] Increased project version
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 87a04243c..b7adc6e61 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.12
+projectVersion=1.1.13
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
From 577c7d3303735d1920553e4a4caa7cb7a5232b31 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominik=20Ro=C4=8Dek?=
<122279630+drocek@users.noreply.github.com>
Date: Fri, 15 Sep 2023 14:00:21 +0200
Subject: [PATCH 13/82] BATM-5359: Trident Wallet as buyOnly (#847)
* BATM-5359: Trident Wallet as buyOnly
---
gradle.properties | 2 +-
server_extensions_extra/src/main/resources/batm-extensions.xml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index b7adc6e61..040ce4d85 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.13
+projectVersion=1.1.14
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
diff --git a/server_extensions_extra/src/main/resources/batm-extensions.xml b/server_extensions_extra/src/main/resources/batm-extensions.xml
index 1d51ae0d7..f50640f75 100644
--- a/server_extensions_extra/src/main/resources/batm-extensions.xml
+++ b/server_extensions_extra/src/main/resources/batm-extensions.xml
@@ -2295,7 +2295,7 @@
apikey is obtained from trongrid.io; privatekey is 64 characters long hex string; feeLimit optional, default 30 [TRX]
- USDTTRON
+ USDTTRON
From 8212fd134cde0d999eb8930c4863752ae3b50925 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Fri, 15 Sep 2023 14:01:27 +0200
Subject: [PATCH 14/82] BPUB-1837 - INotificationListener added Cash collection
created (#839)
---
.../extensions/INotificationListener.java | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java
index f522cc81d..0f1036c17 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/INotificationListener.java
@@ -243,6 +243,31 @@ default void countersShortCleared(String terminalSerialNumber, String cashCollec
*/
default void cashCollectionMissed(String terminalSerialNumber, LocalDate missedCashCollectionDate) {}
+ /**
+ * Invoked when a new cash collection record has been created.
+ *
+ * @param cashCollectionRecord Represents the details of the created cash collection record.
+ *
+ *
+ * ITerminalCashCollectionRecord will have the following attributes populated:
+ *
+ * - Terminal Serial Number: The serial number of the terminal where the cash collection event occurred.
+ * - Terminal Time: Timestamp when event was created on terminal.
+ * - Server Time: Timestamp when event was delivered and stored on server.
+ * - Amounts: A collection of the total amounts in the cashbox, broken down by fiat currency. For example, if the ATM only sells BTC for USD, this will contain one member.
+ * - Collecting Person: Details of the person who performed the cash collection, if available.
+ * - Contains: A string containing a description of what was in the cashbox during the cash collection.
+ * - Note: Any additional text description set by the user via admin.
+ * - Counters Long: The value of the long counter at the time of cash collection.
+ * - Counters Short: The value of the short counter at the time of cash collection before it was reset.
+ * - Cashbox Name: The name of the cashbox.
+ * - Public ID: The public ID of the cash collection.
+ * - Location Public ID: The public ID of the related location.
+ *
+ *
+ */
+ default void cashCollectionCreated(ITerminalCashCollectionRecord cashCollectionRecord) {}
+
default void customerEnrolled(String terminalSerialNumber, String identityPublicId) {}
default void identityCreated(String terminalSerialNumber, String identityPublicId) {}
From bc3634c56f172d2b7dec55b884d1056b6a8cd3dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Fri, 15 Sep 2023 14:02:11 +0200
Subject: [PATCH 15/82] BATM-5350 - Add payment type into transaction detail
(#844)
---
.../batm/server/extensions/ITransactionDetails.java | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java
index 5524e3a6f..df5c2c51b 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionDetails.java
@@ -312,4 +312,9 @@ public interface ITransactionDetails {
* or using {@link IExtensionContext#updateTransaction(String, Integer, String, Map)}.
*/
Map getCustomData();
+
+ /**
+ * @return Payment type: CASH/PAYMENT_CARD
+ */
+ String getPaymentType();
}
From 1a22124eaeb64eecdc151f126ad5daafc7264b73 Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Wed, 4 Oct 2023 12:51:05 +0200
Subject: [PATCH 16/82] LC-241 Added application listener
---
gradle.properties | 2 +-
.../extensions/IApplicationListener.java | 8 +++++++
.../server/extensions/IExtensionContext.java | 4 ++++
.../extensions/event/ApplicationEvent.java | 22 +++++++++++++++++++
.../extensions/TestExtensionContext.java | 10 +++++++++
5 files changed, 45 insertions(+), 1 deletion(-)
create mode 100644 server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IApplicationListener.java
create mode 100644 server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/event/ApplicationEvent.java
diff --git a/gradle.properties b/gradle.properties
index 040ce4d85..bbecb7f86 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.14
+projectVersion=1.1.15
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IApplicationListener.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IApplicationListener.java
new file mode 100644
index 000000000..c7227192b
--- /dev/null
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IApplicationListener.java
@@ -0,0 +1,8 @@
+package com.generalbytes.batm.server.extensions;
+
+import com.generalbytes.batm.server.extensions.event.ApplicationEvent;
+
+public interface IApplicationListener {
+
+ default void onStartup(ApplicationEvent event) {};
+}
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
index d11e088bb..3cfa69391 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
@@ -52,6 +52,10 @@ public interface IExtensionContext {
int PERMISSION_WRITE = 2;
int PERMISSION_EXECUTE = 4;
+ void addApplicationListener(IApplicationListener listener);
+
+ boolean removeApplicationListener(IApplicationListener listener);
+
/**
* Registers listener for listening to transaction events
* @param listener
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/event/ApplicationEvent.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/event/ApplicationEvent.java
new file mode 100644
index 000000000..e6772d193
--- /dev/null
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/event/ApplicationEvent.java
@@ -0,0 +1,22 @@
+package com.generalbytes.batm.server.extensions.event;
+
+public class ApplicationEvent {
+ private boolean extensionsInitialized;
+ private boolean paymentMgrInitialized;
+
+ public boolean isExtensionsInitialized() {
+ return extensionsInitialized;
+ }
+
+ public void setExtensionsInitialized(boolean extensionsInitialized) {
+ this.extensionsInitialized = extensionsInitialized;
+ }
+
+ public boolean isPaymentMgrInitialized() {
+ return paymentMgrInitialized;
+ }
+
+ public void setPaymentMgrInitialized(boolean paymentMgrInitialized) {
+ this.paymentMgrInitialized = paymentMgrInitialized;
+ }
+}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
index f598f4c5e..0673499b7 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
@@ -24,6 +24,16 @@
import java.util.Set;
public class TestExtensionContext implements IExtensionContext {
+ @Override
+ public void addApplicationListener(IApplicationListener listener) {
+
+ }
+
+ @Override
+ public boolean removeApplicationListener(IApplicationListener listener) {
+ return false;
+ }
+
@Override
public void addTransactionListener(ITransactionListener listener) {
From c73c1341436e612a84460ebe62f1e1459fd9e8cd Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Mon, 9 Oct 2023 09:15:26 +0200
Subject: [PATCH 17/82] Update gradle.yml
---
.github/workflows/gradle.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 58e1c59ce..03675addd 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -23,4 +23,4 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
- run: ./gradlew build
+ run: ./gradlew clean build
From 1d8bc8df97f448ff1f82f37cc8a82e3e1dbf7b93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Fri, 13 Oct 2023 16:40:26 +0200
Subject: [PATCH 18/82] LC-249 - Adding states and territories for Australian
locations (#853)
---
.../server/extensions/CountryAustralia.java | 46 +++++++++++++++++++
.../batm/server/extensions/CountryItaly.java | 4 +-
2 files changed, 48 insertions(+), 2 deletions(-)
create mode 100644 server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryAustralia.java
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryAustralia.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryAustralia.java
new file mode 100644
index 000000000..cd410361a
--- /dev/null
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryAustralia.java
@@ -0,0 +1,46 @@
+package com.generalbytes.batm.server.extensions;
+
+/**
+ * Australia province identifiers.
+ *
+ * Usage e.g.:
+ * CountryAustralia.AU-NSW.getProvinceName()
+ * CountryAustralia.valueOf("AU-NSW").getProvinceName()
+ */
+public enum CountryAustralia {
+
+ AU_NSW("AU-NSW", "New South Wales"),
+ AU_QLD("AU-QLD", "Queensland"),
+ AU_SA("AU-SA", "South Australia"),
+ AU_TAS("AU-TAS", "Tasmania"),
+ AU_VIC("AU-VIC", "Victoria"),
+ AU_WA("AU-WA", "Western Australia"),
+ AU_ACT("AU-ACT", "Australian Capital Territory"),
+ AU_NT("AU-NT", "Northern Territory");
+
+ private final String iso;
+
+ private final String provinceName;
+
+ /**
+ * Private constructor.
+ */
+ CountryAustralia(String iso, String provinceName) {
+ this.iso = iso;
+ this.provinceName = provinceName;
+ }
+
+ /**
+ * ISO 3166-2 code of the province (2 digits).
+ */
+ public String getIso() {
+ return iso;
+ }
+
+ /**
+ * English province/territory name officially used by the ISO 3166 Maintenance Agency (ISO 3166/MA).
+ */
+ public String getProvinceName() {
+ return provinceName;
+ }
+}
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryItaly.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryItaly.java
index 49e313ed9..7659b0b20 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryItaly.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/CountryItaly.java
@@ -20,8 +20,8 @@
/**
* Italy province identifiers.
* Usage e.g.:
- * CountryItaly.QC.getProvinceName()
- * CountryItaly.valueOf("QC").getProvinceName()
+ * CountryItaly.AG.getProvinceName()
+ * CountryItaly.valueOf("AG").getProvinceName()
*/
public enum CountryItaly {
AG("AG", "Agrigento"),
From 162789fe51717803cac914ddbe15c50355ac2317 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Fri, 13 Oct 2023 16:40:55 +0200
Subject: [PATCH 19/82] LC-250 - Fixed BitBuy exchange rate calculation (#854)
---
.../bitcoin/exchanges/bitbuy/BitbuyExchange.java | 12 ++----------
1 file changed, 2 insertions(+), 10 deletions(-)
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java
index 3e24f5410..f074f1f19 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/bitcoin/exchanges/bitbuy/BitbuyExchange.java
@@ -140,21 +140,13 @@ private BigDecimal getRateSourceCryptoVolume(String cryptoCurrency) {
@Override
public BigDecimal getExchangeRateForBuy(String cryptoCurrency, String fiatCurrency) {
BigDecimal rateSourceCryptoVolume = getRateSourceCryptoVolume(cryptoCurrency);
- BigDecimal result = calculateBuyPrice(cryptoCurrency, fiatCurrency, rateSourceCryptoVolume);
- if (result != null) {
- return result.divide(rateSourceCryptoVolume, 2, RoundingMode.UP);
- }
- return null;
+ return calculateBuyPrice(cryptoCurrency, fiatCurrency, rateSourceCryptoVolume);
}
@Override
public BigDecimal getExchangeRateForSell(String cryptoCurrency, String fiatCurrency) {
BigDecimal rateSourceCryptoVolume = getRateSourceCryptoVolume(cryptoCurrency);
- BigDecimal result = calculateSellPrice(cryptoCurrency, fiatCurrency, rateSourceCryptoVolume);
- if (result != null) {
- return result.divide(rateSourceCryptoVolume, 2, RoundingMode.DOWN);
- }
- return null;
+ return calculateSellPrice(cryptoCurrency, fiatCurrency, rateSourceCryptoVolume);
}
@Override
From a01f7b7a2696868e637837fd29edc9c6c421ab33 Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Fri, 13 Oct 2023 16:41:57 +0200
Subject: [PATCH 20/82] Increased project version
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index bbecb7f86..59c7948da 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.15
+projectVersion=1.1.16
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
From 372719418f55caafdeb3e688ee5d55860d9d85b5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Mon, 23 Oct 2023 08:37:34 +0200
Subject: [PATCH 21/82] LC-224 - Identity listener improvements (#848)
---
.../server/extensions/IIdentityListener.java | 34 ++++++++++++++++++-
.../identity/ExampleIdentityListener.java | 12 ++++++-
2 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IIdentityListener.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IIdentityListener.java
index fef6ef3e8..5fc99940b 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IIdentityListener.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IIdentityListener.java
@@ -21,6 +21,20 @@
import com.generalbytes.batm.server.extensions.aml.verification.IdentityApplicant;
public interface IIdentityListener {
+
+ /**
+ * @deprecated Use {@link #onIdentityVerificationResult(String, ApplicantCheckResult)} instead.
+ *
+ * @param rawPayload raw data received from the identity verification provider (e.g., in a webhook).
+ * Might be used to access additional data not recognized by the identity verification extension.
+ * @param result data parsed by the identity verification extension.
+ * Contains identity applicant ID that could be used to obtain the Identity,
+ * see {@link IExtensionContext#findIdentityVerificationApplicant(String)}
+ * and {@link IdentityApplicant#getIdentity()}
+ */
+ @Deprecated
+ default void onVerificationResult(String rawPayload, ApplicantCheckResult result) {
+ }
/**
* Called by the server when an identity verification result is received from an identity verification provider.
*
@@ -31,6 +45,24 @@ public interface IIdentityListener {
* see {@link IExtensionContext#findIdentityVerificationApplicant(String)}
* and {@link IdentityApplicant#getIdentity()}
*/
- default void onVerificationResult(String rawPayload, ApplicantCheckResult result) {
+ default void onIdentityVerificationResult(String rawPayload, ApplicantCheckResult result) {
+ }
+
+ /**
+ * Called when a new identity is created by the master or admin service.
+ *
+ * @param publicIdentityId Public ID of the newly created identity.
+ */
+ default void onIdentityCreated(String publicIdentityId) {
+ }
+
+ /**
+ * Called whenever the admin or master service changes the state of an identity.
+ *
+ * @param publicIdentityId Public ID of the identity whose state has changed.
+ * @param stateFrom The previous state of the identity before the change.
+ * @param stateTo The new state of the identity after the change.
+ */
+ default void onIdentityStateChanged(String publicIdentityId, int stateFrom, int stateTo) {
}
}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/ExampleIdentityListener.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/ExampleIdentityListener.java
index ffaaa42e9..e65f62b72 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/ExampleIdentityListener.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/examples/identity/ExampleIdentityListener.java
@@ -28,7 +28,7 @@ public class ExampleIdentityListener implements IIdentityListener {
Logger log = LoggerFactory.getLogger(ExampleIdentityListener.class);
@Override
- public void onVerificationResult(String rawPayload, ApplicantCheckResult result) {
+ public void onIdentityVerificationResult(String rawPayload, ApplicantCheckResult result) {
WebhookData data = parsePayload(rawPayload);
IExtensionContext ctx = IdentityExampleExtension.getExtensionContext();
IdentityApplicant applicant = ctx.findIdentityVerificationApplicant(result.getIdentityApplicantId());
@@ -86,6 +86,16 @@ public void onVerificationResult(String rawPayload, ApplicantCheckResult result)
new ChoiceCustomFieldValue(element.getId())));
}
+ @Override
+ public void onIdentityCreated(String publicIdentityId) {
+ log.info("Identity with public ID {} has been created", publicIdentityId);
+ }
+
+ @Override
+ public void onIdentityStateChanged(String publicIdentityId, int stateFrom, int stateTo) {
+ log.info("Identity with public ID {} changed from state {} to state {}", publicIdentityId, stateFrom, stateTo);
+ }
+
private CustomFieldDefinition findCustomFieldDefinition(Collection customFieldDefinitions,
String name,
CustomFieldDefinitionType type) {
From 4776364c82494c67cd1b94468b6661aa6c4761ad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Mon, 23 Oct 2023 08:38:01 +0200
Subject: [PATCH 22/82] BATM-5013 - LND sendpayment should contain remote
transaction id (#849)
---
.../lightningbitcoin/wallets/lnd/LndWallet.java | 10 ++++++++++
.../wallets/lnd/dto/Payment.java | 17 ++++++++++++++++-
.../wallets/lnd/LndWalletTest.java | 2 +-
3 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWallet.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWallet.java
index 69c7f962d..0471730ec 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWallet.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWallet.java
@@ -39,6 +39,7 @@
import java.net.ConnectException;
import java.security.GeneralSecurityException;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -81,6 +82,7 @@ public String sendCoins(String destinationAddress, BigDecimal amount, String cry
payment.amt = CoinUnit.bitcoinToSat(amount).toString();
payment.payment_request = destinationAddress;
payment.fee_limit = getFeeLimit(feeLimit);
+ payment.dest_custom_records = getCustomRecords(description);
log.info("Sending payment: {}", payment);
SendPaymentResponse paymentResponse = callChecked(() -> api.sendPayment(payment));
@@ -111,6 +113,14 @@ private Payment.FeeLimit getFeeLimit(String fee) {
return feeLimit;
}
+ private static Map getCustomRecords(String description) {
+ Map customRecords = new HashMap<>();
+ if (description != null && !description.trim().isEmpty()) {
+ customRecords.put(1L, description.getBytes());
+ }
+ return customRecords;
+ }
+
@Override
public String getInvoice(BigDecimal cryptoAmount, String cryptoCurrency, Long paymentValidityInSec, String description) {
Invoice invoice = new Invoice();
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/dto/Payment.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/dto/Payment.java
index 74cb8c4b4..f14ef05fe 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/dto/Payment.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/dto/Payment.java
@@ -17,6 +17,9 @@
************************************************************************************/
package com.generalbytes.batm.server.extensions.extra.lightningbitcoin.wallets.lnd.dto;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
public class Payment {
/**
* A bare-bones invoice for a payment within the Lightning Network. With the details of the invoice, the sender has all the data necessary to send a payment to the recipient
@@ -34,6 +37,14 @@ public class Payment {
*/
public FeeLimit fee_limit;
+ /**
+ * An optional field that can be used to pass an arbitrary set of TLV records to a peer which understands the new records.
+ * This can be used to pass application specific data during the payment attempt.
+ * Record types are required to be in the custom range >= 65536.
+ * When using REST, the values must be encoded as base64.
+ */
+ public Map dest_custom_records;
+
public static class FeeLimit {
/**
* The fee limit expressed as a fixed amount of satoshis
@@ -52,6 +63,10 @@ public String toString() {
@Override
public String toString() {
- return "Payment{" + amt + " sat, fee limit: " + fee_limit + " to " + payment_request + '}';
+ String rid = "";
+ if (dest_custom_records != null && !dest_custom_records.containsKey(1L)) {
+ rid = new String(dest_custom_records.get(1L), StandardCharsets.UTF_8);
+ }
+ return "Payment{" + amt + " sat, fee limit: " + fee_limit + " to " + payment_request + ", rid: " + rid + '}';
}
}
diff --git a/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWalletTest.java b/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWalletTest.java
index a08ab3057..84a1c7ab7 100644
--- a/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWalletTest.java
+++ b/server_extensions_extra/src/test/java/com/generalbytes/batm/server/extensions/extra/lightningbitcoin/wallets/lnd/LndWalletTest.java
@@ -21,7 +21,7 @@ public LndWalletTest() throws GeneralSecurityException {
public void sendCoins() {
IWalletInformation i = w.getWalletInformation();
String paymentHash = w.sendCoins("LNBC1U1PWJMJJNPP5YRDV8EPPK74UZ69QVHK940EHE9469H8GHXE626MZGCLZ202REZKQDPY2PKXZ7FQVYSXWCTDV5SX7E3QWD3HYCT5VD5QCQZPGQNL8L0227LDNLEJA3HQWUFHF788ADMD640YKFZ8A9FAGYG4RQE7XGC4CXFMTVU8SAWPE3WVU8WNUHW52R6LSD4797RZ0DPMPTHH3K0CQ378HK2",
- new BigDecimal("0.000001"), CryptoCurrency.LBTC.getCode(), "");
+ new BigDecimal("0.000001"), CryptoCurrency.LBTC.getCode(), "R23V4C");
System.out.println(paymentHash);
}
From 445d5336f9b1af96e3ec9d56b8d978d8b7e7f9a7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Mon, 23 Oct 2023 08:38:24 +0200
Subject: [PATCH 23/82] BPUB-1993 - Add overrideTransactionPreparation on
ITransactionListener (#855)
---
.../extensions/ITransactionListener.java | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionListener.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionListener.java
index 67d9fccf4..535f7bb35 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionListener.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/ITransactionListener.java
@@ -29,6 +29,24 @@ default boolean isTransactionPreparationApproved(ITransactionPreparation prepara
return true;
}
+ /**
+ * Allows the operator to override following values in {@link ITransactionPreparation}.
+ *
+ * - cryptoAddress
+ * - cashTransactionLimitWithName
+ * - cashTransactionMinimum
+ * - supplyTransactionLimit
+ * - allowedDiscountCode
+ *
+ * This method is called for both BUY and SELL transactions.
+ *
+ * @param preparation The transaction preparation details, including calculated values.
+ * @return {@link ITransactionPreparation} that may contain modified transaction details.
+ */
+ default ITransactionPreparation overrideTransactionPreparation(ITransactionPreparation preparation) {
+ return preparation;
+ }
+
/**
* Callback method that is called by server before transaction is executed - however the cash is already inserted in machine in case of buy transaction.
* If your method returns false than transaction will not take place and will fail with error ERROR_NOT_APPROVED.
From fecae7d1e73127bde23c36ba1aacb277fe225cf8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maro=C5=A1=20Silady?=
<38668012+SMaros@users.noreply.github.com>
Date: Mon, 23 Oct 2023 08:38:57 +0200
Subject: [PATCH 24/82] CF-568 - Transaction Unlock Time in Output Queue
Feature (#852)
---
.../batm/server/extensions/IExtensionContext.java | 10 ++++++++++
.../server/extensions/OutputQueueInsertConfig.java | 12 ++++++++++++
.../batm/server/extensions/TestExtensionContext.java | 4 ++++
3 files changed, 26 insertions(+)
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
index 3cfa69391..648d14360 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/IExtensionContext.java
@@ -831,4 +831,14 @@ public static class EmbeddedEmailImage {
* @param remoteOrLocalTransactionId
*/
void markTransactionAsWithdrawn(String remoteOrLocalTransactionId);
+
+ /**
+ * Allows to manage the unlock time of a specific transaction.
+ * This can be used to unlock the transaction sooner or prolong it.
+ * Transactions that are locked will remain in the output queue and won't be flushed until they are unlocked.
+ *
+ * @param rid Remote transaction ID
+ * @param serverTimeToUnlock The absolute datetime when the transaction should unlock.
+ */
+ void unlockTransaction(String rid, Date serverTimeToUnlock);
}
diff --git a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/OutputQueueInsertConfig.java b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/OutputQueueInsertConfig.java
index 7586f457d..7fefbb31b 100644
--- a/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/OutputQueueInsertConfig.java
+++ b/server_extensions_api/src/main/java/com/generalbytes/batm/server/extensions/OutputQueueInsertConfig.java
@@ -17,6 +17,8 @@
************************************************************************************/
package com.generalbytes.batm.server.extensions;
+import java.util.Date;
+
/**
* Information about how a transaction should be inserted into an Output Queue.
* This is initially configured in admin per Output Queue and it can be overridden by extensions for each transaction.
@@ -32,6 +34,7 @@ public class OutputQueueInsertConfig {
private boolean secondaryManualApprovalRequired = false;
private int secondaryDelaySeconds = 0;
+ private Date unlockTime;
public boolean isSkipQueue() {
return skipQueue;
@@ -81,6 +84,14 @@ public void setSecondaryDelaySeconds(int secondaryDelaySeconds) {
this.secondaryDelaySeconds = secondaryDelaySeconds;
}
+ public Date getUnlockTime() {
+ return unlockTime;
+ }
+
+ public void setUnlockTime(Date unlockTime) {
+ this.unlockTime = unlockTime;
+ }
+
@Override
public String toString() {
return "OutputQueueInsertConfig{" +
@@ -90,6 +101,7 @@ public String toString() {
", insertIntoSecondaryQueue=" + insertIntoSecondaryQueue +
", secondaryManualApprovalRequired=" + secondaryManualApprovalRequired +
", secondaryDelaySeconds=" + secondaryDelaySeconds +
+ ", unlockTime=" + unlockTime +
'}';
}
}
diff --git a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
index 0673499b7..3b0c6cd60 100644
--- a/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
+++ b/server_extensions_extra/src/main/java/com/generalbytes/batm/server/extensions/TestExtensionContext.java
@@ -481,4 +481,8 @@ public boolean isGlobalServer() {
public void markTransactionAsWithdrawn(String remoteOrLocalTransactionId) {
}
+
+ @Override
+ public void unlockTransaction(String rid, Date serverTimeToUnlock) {
+ }
}
From 6709a8ae119bffc6be6b146402302921414bd2fd Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Mon, 23 Oct 2023 08:47:19 +0200
Subject: [PATCH 25/82] Increased project version
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 59c7948da..f55190239 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
# buildscript - project id
projectGroup=com.generalbytes.batm.public
-projectVersion=1.1.16
+projectVersion=1.1.17
# buildscript - common dependency versions
bitrafaelVersion=1.0.44
From 6a208b23eac1d5856a205c78d82209be595607bb Mon Sep 17 00:00:00 2001
From: Filip Ocelka
Date: Tue, 24 Oct 2023 11:18:17 +0200
Subject: [PATCH 26/82] BATM-5470 Upgrade gradle to version 8.3 (#851)
---
annotations/build.gradle | 33 +-
batm_ssh_tunnel/build.gradle | 25 +-
build.gradle | 71 -
buildSrc/build.gradle | 7 +
.../gradle/plugin/GBGradlePlugin.groovy | 2 +-
.../groovy/shared-build-conventions.gradle | 40 +
.../groovy/shared-publish-conventions.gradle | 29 +
...shared-repositories-ext-conventions.gradle | 24 +
currencies/build.gradle | 39 +-
dependencySubstitutions.txt | 22 +-
gradle/verification-metadata.xml | 1980 +++++++++++++++++
gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 59203 bytes
gradle/wrapper/gradle-wrapper.properties | 2 +-
gradlew | 37 +-
gradlew.bat | 27 +-
operators_sample_website/build.gradle | 61 +-
server_extensions_api/build.gradle | 52 +-
server_extensions_extra/build.gradle | 169 +-
server_extensions_test/build.gradle | 30 +-
verification_site/build.gradle | 31 +-
.../gradle/wrapper/gradle-wrapper.properties | 2 +-
21 files changed, 2259 insertions(+), 424 deletions(-)
delete mode 100644 build.gradle
create mode 100644 buildSrc/build.gradle
create mode 100644 buildSrc/src/main/groovy/shared-build-conventions.gradle
create mode 100644 buildSrc/src/main/groovy/shared-publish-conventions.gradle
create mode 100644 buildSrc/src/main/groovy/shared-repositories-ext-conventions.gradle
create mode 100644 gradle/verification-metadata.xml
diff --git a/annotations/build.gradle b/annotations/build.gradle
index ac636b1f0..e47c2d3bb 100644
--- a/annotations/build.gradle
+++ b/annotations/build.gradle
@@ -1,34 +1,7 @@
-apply plugin: "java"
-apply plugin: "com.generalbytes.gradle.main"
-apply plugin: 'maven-publish'
+plugins {
+ id("shared-build-conventions")
+}
group = projectGroup
version = projectVersion
-publishing {
- if (hasGbUploadArtifactory) {
- repositories {
- maven {
- credentials {
- username gbArtifactoryUser
- password gbArtifactoryPassword
- }
- url gbArtifactoryUploadUrl
- }
- }
- }
-
- publications {
- mavenJava(MavenPublication) {
- from components.java
- }
- }
-}
-
-dependencySubstitutions {
- substitute file(batmDependencySubstitutionConfig)
-}
-dependencyVerifications {
- checksums batmDependencyChecksumsConfig
- printUnusedAssertions false
-}
\ No newline at end of file
diff --git a/batm_ssh_tunnel/build.gradle b/batm_ssh_tunnel/build.gradle
index 0089779d4..3fede2f26 100644
--- a/batm_ssh_tunnel/build.gradle
+++ b/batm_ssh_tunnel/build.gradle
@@ -1,10 +1,16 @@
-apply plugin: "java"
-apply plugin: 'application'
+plugins {
+ id("application")
+ id("shared-build-conventions")
+}
+
+repositories {
+ mavenCentral()
+}
version = "1.0.0"
application {
- mainClassName = 'com.generalbytes.batm.sshtunnel.Main'
+ mainClass = "com.generalbytes.batm.sshtunnel.Main"
}
distributions {
@@ -18,12 +24,13 @@ distributions {
}
}
+configurations.configureEach {
+ exclude group: "org.eclipse.ee4j"
+}
dependencies {
- compile(group: 'org.apache.sshd', name: 'sshd-core', version: '2.3.0')
- compile(group: 'org.apache.sshd', name: 'sshd-common', version: '2.3.0')
- compile(group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3')
-
- compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.63'
-
+ implementation("org.apache.sshd:sshd-core:2.3.0")
+ implementation("org.apache.sshd:sshd-common:2.3.0")
+ implementation("ch.qos.logback:logback-classic:1.2.9")
+ implementation("org.bouncycastle:bcpkix-jdk15on:1.63")
}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 5ac8e3025..000000000
--- a/build.gradle
+++ /dev/null
@@ -1,71 +0,0 @@
-buildscript {
- ext.with {
- hasGbArtifactory = (project.hasProperty('gbArtifactoryUrl')
- && project.hasProperty('gbArtifactoryUser')
- && project.hasProperty('gbArtifactoryPassword'))
-
- hasGbUploadArtifactory = (project.hasProperty('gbArtifactoryUploadUrl')
- && project.hasProperty('gbArtifactoryUser')
- && project.hasProperty('gbArtifactoryPassword'))
-
- setRepositoriesFor = { RepositoryHandler repositoryHandler ->
- if (hasGbArtifactory) {
- repositoryHandler.maven {
- credentials {
- username gbArtifactoryUser
- password gbArtifactoryPassword
- }
- url gbArtifactoryUrl
- }
- }
- repositoryHandler.mavenCentral()
- repositoryHandler.maven { url 'https://jitpack.io' }
- }
- batmDependencySubstitutionConfig = file('dependencySubstitutions.txt')
- batmDependencyChecksumsConfig = file('dependencyChecksums.txt')
- }
- setRepositoriesFor(repositories)
-}
-
-allprojects {
- setRepositoriesFor(buildscript.repositories)
- setRepositoriesFor(repositories)
- repositories {
- //bitcoin-json-rpc-client-1.0.jar isn't part of any well known maven repo
- //so we search the libs dir; gradle generates (guesses) metadata except dependencies.
- //Artifacts from repos with real metadata take precedence.
- flatDir {
- dirs rootProject.file('libs')
- }
- }
- tasks.withType(JavaCompile) {
- project.sourceCompatibility = JavaVersion.VERSION_1_8
- options.encoding = 'UTF-8'
- options.incremental = true
- }
- // Jars with the same contents should have the same checksums
- tasks.withType(AbstractArchiveTask).configureEach {
- preserveFileTimestamps = false
- reproducibleFileOrder = true
- }
- afterEvaluate {
- if (pluginManager.hasPlugin('com.generalbytes.gradle.dependency.verification')) {
- dependencyChecksums {
- global = true
- }
- dependencyVerifications {
-// setConfigurations([]) // effectively turns off all checksum verifications
-// failOnChecksumError = false // turns off failure on checksum problem (only warnings are printed)
- }
- }
- if (pluginManager.hasPlugin('com.generalbytes.gradle.dependency.strict')) {
- strictDependencies {
-// conflictFail false // turns off failure on version conflict
- }
- }
- }
-
- wrapper {
- distributionType = Wrapper.DistributionType.ALL
- }
-}
\ No newline at end of file
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
new file mode 100644
index 000000000..79ef21895
--- /dev/null
+++ b/buildSrc/build.gradle
@@ -0,0 +1,7 @@
+plugins {
+ id 'groovy-gradle-plugin'
+}
+
+repositories {
+ gradlePluginPortal()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/com/generalbytes/gradle/plugin/GBGradlePlugin.groovy b/buildSrc/src/main/groovy/com/generalbytes/gradle/plugin/GBGradlePlugin.groovy
index acc2cc3d5..9c87fe8c4 100644
--- a/buildSrc/src/main/groovy/com/generalbytes/gradle/plugin/GBGradlePlugin.groovy
+++ b/buildSrc/src/main/groovy/com/generalbytes/gradle/plugin/GBGradlePlugin.groovy
@@ -63,7 +63,7 @@ class GBGradlePlugin implements Plugin {
project.dependencyVerifications.configuration('releaseRuntimeClasspath')
project.dependencyVerifications.configuration('debugRuntimeClasspath')
} else if (pluginMgr.hasPlugin('org.gradle.java')) {
- project.dependencyVerifications.configuration('runtime')
+ project.dependencyVerifications.configuration('runtimeClasspath')
}
final Configuration buildScriptClasspathConfiguration =
diff --git a/buildSrc/src/main/groovy/shared-build-conventions.gradle b/buildSrc/src/main/groovy/shared-build-conventions.gradle
new file mode 100644
index 000000000..1fb725fef
--- /dev/null
+++ b/buildSrc/src/main/groovy/shared-build-conventions.gradle
@@ -0,0 +1,40 @@
+plugins {
+ id("java")
+ id("com.generalbytes.gradle.main")
+ id("shared-publish-conventions")
+}
+
+repositories {
+ mavenCentral()
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.encoding = "UTF-8"
+ options.incremental = true
+}
+
+dependencySubstitutions {
+ substitute file("$project.rootDir/dependencySubstitutions.txt")
+}
+
+tasks.withType(AbstractArchiveTask).configureEach {
+ preserveFileTimestamps = false
+ reproducibleFileOrder = true
+}
+
+afterEvaluate {
+ if (pluginManager.hasPlugin('com.generalbytes.gradle.dependency.verification')) {
+ dependencyChecksums {
+ global = true
+ }
+ dependencyVerifications {
+ setConfigurations([]) // effectively turns off all checksum verifications - replaced by gradle's verification-metadata.xml
+ }
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/shared-publish-conventions.gradle b/buildSrc/src/main/groovy/shared-publish-conventions.gradle
new file mode 100644
index 000000000..b04143398
--- /dev/null
+++ b/buildSrc/src/main/groovy/shared-publish-conventions.gradle
@@ -0,0 +1,29 @@
+plugins {
+ id("maven-publish")
+}
+
+publishing {
+ if (hasGbUploadArtifactory()) {
+ repositories {
+ maven {
+ credentials {
+ username gbArtifactoryUser
+ password gbArtifactoryPassword
+ }
+ url gbArtifactoryUploadUrl
+ }
+ }
+ }
+
+ publications {
+ mavenJava(MavenPublication) {
+ from components.java
+ }
+ }
+}
+
+private boolean hasGbUploadArtifactory() {
+ return project.hasProperty("gbArtifactoryUploadUrl")
+ && project.hasProperty("gbArtifactoryUser")
+ && project.hasProperty("gbArtifactoryPassword")
+}
diff --git a/buildSrc/src/main/groovy/shared-repositories-ext-conventions.gradle b/buildSrc/src/main/groovy/shared-repositories-ext-conventions.gradle
new file mode 100644
index 000000000..67cae8a8b
--- /dev/null
+++ b/buildSrc/src/main/groovy/shared-repositories-ext-conventions.gradle
@@ -0,0 +1,24 @@
+ext {
+ hasGbArtifactory = hasGbArtifactory()
+}
+
+repositories {
+ maven {
+ url = uri("https://jitpack.io")
+ }
+ if (hasGbArtifactory) {
+ maven {
+ url = uri(project.findProperty("gbArtifactoryUrl").toString())
+ credentials {
+ username = project.findProperty("gbArtifactoryUser").toString()
+ password = project.findProperty("gbArtifactoryPassword").toString()
+ }
+ }
+ }
+}
+
+private boolean hasGbArtifactory() {
+ return project.hasProperty('gbArtifactoryUrl')
+ && project.hasProperty('gbArtifactoryUser')
+ && project.hasProperty('gbArtifactoryPassword')
+}
\ No newline at end of file
diff --git a/currencies/build.gradle b/currencies/build.gradle
index 64a7f6d69..d0ffed15d 100644
--- a/currencies/build.gradle
+++ b/currencies/build.gradle
@@ -1,40 +1,11 @@
-apply plugin: "java"
-apply plugin: "com.generalbytes.gradle.main"
-apply plugin: 'maven-publish'
+plugins {
+ id("shared-build-conventions")
+}
group = projectGroup
version = projectVersion
-
dependencies {
- compile(group: 'org.slf4j', name: 'slf4j-api', version: '1.7.28')
- testCompile (group: 'junit', name: 'junit', version: '4.13.1')
-}
-
-publishing {
- if (hasGbUploadArtifactory) {
- repositories {
- maven {
- credentials {
- username gbArtifactoryUser
- password gbArtifactoryPassword
- }
- url gbArtifactoryUploadUrl
- }
- }
- }
-
- publications {
- mavenJava(MavenPublication) {
- from components.java
- }
- }
-}
-
-dependencySubstitutions {
- substitute file(batmDependencySubstitutionConfig)
-}
-dependencyVerifications {
- checksums batmDependencyChecksumsConfig
- printUnusedAssertions false
+ implementation("org.slf4j:slf4j-api:1.7.28")
+ testImplementation("junit:junit:4.13.1")
}
\ No newline at end of file
diff --git a/dependencySubstitutions.txt b/dependencySubstitutions.txt
index 43b827b55..57fbad937 100644
--- a/dependencySubstitutions.txt
+++ b/dependencySubstitutions.txt
@@ -1,18 +1,17 @@
// generated at 2021-05-26T11:51:06
// shared substitutions (project ':currencies')
-substitute module: 'com.fasterxml.jackson.core:jackson-annotations', versions: ['2.9.1', '2.9.9', '2.11.2'], toVersion: '2.12.0'
-substitute module: 'com.fasterxml.jackson.core:jackson-core', versions: ['2.7.4', '2.9.9', '2.11.2'], toVersion: '2.12.0'
-substitute module: 'com.fasterxml.jackson.core:jackson-databind', versions: ['2.7.4', '2.8.5', '2.9.6', '2.9.9', '2.10.0', '2.11.2'], toVersion: '2.12.0'
+substitute module: 'com.fasterxml.jackson.core:jackson-databind', versions: ['2.7.4', '2.9.6', '2.9.9', '2.10.0', '2.11.2'], toVersion: '2.12.0'
+substitute module: 'com.fasterxml.jackson.core:jackson-core', versions: ['2.7.4', '2.11.2'], toVersion: '2.12.0'
+substitute module: 'com.fasterxml.jackson.core:jackson-databind', versions: ['2.7.4', '2.9.6', '2.9.9', '2.10.0', '2.11.2'], toVersion: '2.12.0'
substitute module: 'com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider', versions: ['2.8.5'], toVersion: '2.12.0'
-substitute module: 'com.github.jnr:jnr-ffi', versions: ['2.1.1'], toVersion: '2.1.2'
+substitute module: 'com.fasterxml.jackson.core:jackson-annotations', versions: ['2.9.1', '2.11.2'], toVersion: '2.12.0'
substitute module: 'com.github.mmazi:rescu', versions: ['2.0.2'], toVersion: '2.1.0'
substitute module: 'com.google.code.findbugs:jsr305', versions: ['2.0.1'], toVersion: '3.0.2'
-substitute module: 'com.google.errorprone:error_prone_annotations', versions: ['2.2.0', '2.3.3', '2.3.4', '2.5.1'], toVersion: '2.7.1'
-substitute module: 'com.google.guava:guava', versions: ['21.0', '27.0.1-android', '28.1-android', '28.2-android', '29.0-android', '29.0-jre', '30.0-android', '30.1.1-jre', '31.0.1-android'], toVersion: '31.0.1-jre'
-substitute module: 'com.google.j2objc:j2objc-annotations', versions: ['1.1'], toVersion: '1.3'
+substitute module: 'com.google.errorprone:error_prone_annotations', versions: ['2.3.3', '2.3.4'], toVersion: '2.7.1'
+substitute module: 'com.google.guava:guava', versions: ['21.0', '27.0.1-android', '28.1-android', '29.0-android', '29.0-jre', '30.0-android', '30.1.1-jre', '31.0.1-android'], toVersion: '31.0.1-jre'
substitute module: 'com.google.protobuf:protobuf-java', versions: ['2.6.1', '3.6.1', '3.11.0', '3.12.0'], toVersion: '3.13.0'
substitute module: 'com.madgag.spongycastle:core', versions: ['1.51.0.0', '1.52.0.0'], toVersion: '1.58.0.0'
-substitute module: 'com.squareup.okhttp3:okhttp', versions: ['3.8.1', '3.12.1', '3.12.8', '3.12.12', '3.14.9', '4.2.2'], toVersion: '4.9.0'
+substitute module: 'com.squareup.okhttp3:okhttp', versions: ['3.12.1', '3.12.12', '3.14.9', '4.2.2'], toVersion: '4.9.0'
substitute module: 'com.squareup.okhttp:okhttp', versions: ['2.7.2'], toVersion: '2.7.4'
substitute module: 'com.squareup.okio:okio', versions: ['1.6.0', '1.13.0', '1.16.0'], toVersion: '2.8.0'
substitute module: 'commons-codec:commons-codec', versions: ['1.3', '1.9', '1.11'], toVersion: '1.13'
@@ -29,15 +28,14 @@ substitute module: 'io.netty:netty-transport', versions: ['4.1.8.Final'], toVers
substitute module: 'javax.ws.rs:javax.ws.rs-api', versions: ['2.0.1'], toVersion: '2.1'
substitute module: 'javax.annotation:javax.annotation-api', versions: ['1.2'], toVersion: '1.3.2'
substitute module: 'org.apache.commons:commons-lang3', versions: ['3.5'], toVersion: '3.12.0'
-substitute module: 'org.bouncycastle:bcprov-jdk15on', versions: ['1.54', '1.60', '1.66', '1.68'], toVersion: '1.63'
-substitute module: 'org.codehaus.mojo:animal-sniffer-annotations', versions: ['1.17'], toVersion: '1.18'
+substitute module: 'org.bouncycastle:bcprov-jdk15on', versions: ['1.60', '1.66', '1.68'], toVersion: '1.63'
substitute module: 'org.jetbrains.kotlin:kotlin-stdlib-jdk8', versions: ['1.3.31'], toVersion: '1.4.10'
substitute module: 'org.jetbrains.kotlin:kotlin-stdlib', versions: ['1.4.0'], toVersion: '1.4.10'
substitute module: 'org.jetbrains.kotlin:kotlin-stdlib-common', versions: ['1.4.0'], toVersion: '1.4.10'
substitute module: 'org.json:json', versions: ['20140107'], toVersion: '20180130'
-substitute module: 'org.slf4j:slf4j-api', versions: ['1.7.20', '1.7.21', '1.7.25', '1.7.30', '1.7.32', '1.7.33', '1.8.0-beta4'], toVersion: '1.7.28'
+substitute module: 'org.slf4j:slf4j-api', versions: ['1.7.20', '1.7.21', '1.7.25', '1.7.26', '1.7.30', '1.7.32', '1.7.33', '1.8.0-beta4'], toVersion: '1.7.28'
substitute module: 'org.knowm.xchange:xchange-core', versions: ['5.0.12', '5.0.13'], toVersion: '5.0.10'
-substitute module: 'org.bouncycastle:bcprov-jdk15to18', versions: ['1.66','1.68'], toVersion: '1.69'
+substitute module: 'org.bouncycastle:bcprov-jdk15to18', versions: ['1.66'], toVersion: '1.69'
substitute module: 'com.fasterxml.jackson.datatype:jackson-datatype-guava', versions: ['2.11.2'], toVersion: '2.12.0'
substitute module: 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8', versions: ['2.11.2'], toVersion: '2.12.0'
substitute module: 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310', versions: ['2.11.2'], toVersion: '2.12.0'
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
new file mode 100644
index 000000000..068423da7
--- /dev/null
+++ b/gradle/verification-metadata.xml
@@ -0,0 +1,1980 @@
+
+
+
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644
GIT binary patch
delta 23334
zcmZ6yQ*_^7)b$%Swr#tyZQHhuU-WHk+qUgAc4J!&nxrusy#I5a=UlvJjD59l*Pe6C
zy*_IVG(!&0LN+phBc)L-m3M)If#E@dfw80{QedYjfnx%cY|Q2krta=>YST_jBA9|p
zot|vvp%0RvR1srYTl+z-NNCL@5oSg;&!BaMOR}sfJn192cT55<(x!dL7ut~~3^-Ur
z4>ora_t}-M=h->qJpjxnx)1EWvn8?z{O>`3f+7iuKL<2+zHP~ldyrmD0P{Z4X%%`W
zo_)z~Yy==^IcLFQUXFGeH8WebVkw~L>r{vkbd$z5MQq(ni#a^*>hw=_Z;C^Gfrdev
z!mgg_pG
zeMQUU+?X~Em$z2qQyLw%`*oeVS_0m|fcm)7q6xUbNU;Eku2#8)2t3}hj!-y+-89iQ
z3fZ2srkJN7rV0vd0?Or&O+;oeJrGw6+{`LpB@d3*VpO>Un|q3BNDJspjozc(4hJDz
zwgOl$df!`k*;k(~&;GPfVBAD3Hi3C}ZFV~#*$f>4hj%YsCq6tRQfp_Dt-)S_Uj!o=
ze~fwe`&6h3{1?2yCfi
zXybknxod^Z|~hQkrhOl74q
z$G@Js5lv&IFx8Sm%&;&R^ZS012w;u(#-d_d7z}E<_L7JxsnmzL7!JXpt9>W$Br_-E
zrt)8pGV-SsMKD!epNc6VMP@dY9SZ~}4KEJ0{AM}D(Ur&6>Xwy(7hK_??ybcBfV^H
zx_aQ9cAG-(o3ZK6^5ob$c;XQ+WUNPojJo*4bQPb@#nF;E%h&FNJuVpSRK{}ljl}!b
z#w$tS(t%=z)Q_2_4&C(JNz3Z&rgJG<@$5eR{6=#eNx!WXg2rrliM1=mC{vw4N32Vt
z(hz+({@Wh2Y$x_R-d{$2XdqlCZW<@Yvix3|nho{g3fcY`x3r&v
zC3T%<=pJrdP1&am@lIKma2=I=^4+>BZP8iAC+!5rKrxkP-K0t^lPkRKzej86htd0P
z#d#*bI0LJ?=)BWl*(f{h=~UK26R;3?r6Z!LAuS$vtfd9{cVHb61Hh{>!#phiJ%Th9
zF?=-pJ;B(60kgq8M!6s_=E5q^V1BZqUk45QP(0*!5vKTDdWw8Z2W(yF7Cd4q6#8Au
zDKAwS7y&OlW39}KP7u;mRY_qmKm6ZlbFdopRZRb2WvuPtfGOrS@2QJ&4I=v~NILZ5
zeRhAPI(ofewJkMGXux=19@_Z8{!gjzB73;zNpU}X|DXwxK^;Cvj0Ph3u|D+PK~V7Z
z?T_+HtO$qw$Y7Eiis5+%de#S_2Eg{NT?gs+rEQ*+9;JM`;i65mGIf65%GmAWA1&vF
zlc?PlDec;zALdLmib;DC&8{{TV>uUmnkgCuNg83d=~K)66oA^Xl2_g3joQ7h45dDe
zhrM9pl;y7z>d~B9=jQH;Q=2Fr{5!6n4(@U2+i4B!LnEVpkskhl8Y&h?h2<}2MvUa(Z=c-L0$s#VLm_n6MN={uuQNF?aO%NJt-w^*Q^v38n
zSik;)49a!p_y;?PBm+2+r&6d%&w5wFcSS3i(Q0})76N`VU$9#xpY*=PpEvRJL*_v?
zq`fJn6uibh+U?Oh=7TngAZ+QgfVq{*FP4XT@%T4DJXQ3^Q%|A#S*bgV=uQOkLs3B>
zPb@_|qGW^GJGUz;Rdk=&!X5<@+IA_92osMhzl2w&pZpOkH2wg6{QNKJ_SprLV)J7~
zswn~v{%5cFd4Dchvot~B4Q=>*(PzriPyl!KvQ;DQT4Jwc7b
z@=RK6_wy*9Ls}eOd#i_ifu-1gyG1I4B$wrf0s~uz`Oi=PUk3$X;9w*ytxP=~JW?)j
ziGecB9d!at%>E`;fCYBIE`?LXQ%q2#KyT1)F3gKTVQ(^OFF_%e>U9C|Jftsp-L
z-uBgv--?x$jQ!7JVOO%A6s_NIULK3t`AUvLNRGy1+2c=*hNLTgEU{(f`aS3R&0c#8
zJ)H~+lk7p>Antxg8%KDw8HA(zRyL7IsRXPZq(&|IG=anACS|u!&ze?(596{Wa^56I
z(Hh0)W(B=vPMB&$-+voJG+&MRo0U(|tiC0?n~hObfB_lJ_0ohe01y+)rhiRx-dz
zr-Ddvfh`2n6^
zE<#-hLF2)fS!S>(AgaU7)DA<}B0gb;cUhr}#B$zitS3?I
zQ2dfsjc&|!;>ZmeP`tUDacf0iky2%{sdnvR10i;nHt{`{s%AE_Ck=O!`CgKV{TxZt
zvGG&6h(`32V2E)jIe5jAb7h61MnLCplX!amDU*7b478F^m0qqf96LN3N^S2xtX@WV
zqjdFPUpJ(hHl4?SW`Rxi^WJaHe&^dS6OY9@unu!n*p3<-W-CQ>pb^E?XzN3;LFQ%}E-2`SgWHo)7f-p+JMy`RG3E&3PwN54o9wVP*Nq{9PKSNP@R_eO
zKB~SbZXrKS%qqUV1h!p7JvFb&fbotnqw2Q5-wA7wlEq4H?+^~Js$F8pms&<$wDQtJ
zl0cD0WH*i-3Lza6dDXZ-#eh8JlXkv(BGQT%ufa%jHyi2P_PS;2Q-5b!JPW(HoNzYg
z2(g^gwcm)p-Q2=kK{=bNP4d6yB|A(BM{w}7e~-*Rt}#Z0uO{Xa=nY%!B|uW5EG{vg
zbLt&cVKr)8e;2Fjx3r;i#5>@hs!6e6@JKF5xyGp+)QM4t?M}2m%79NOpKi>$f_G
zEbVBL#9J#iY7hDnU;}~%>)&&6NL$+Y}5cc(#RW7pC-r5LDH|vnfahGt*C$(Ng4D
z@UDxQAtvS2YmtXYUy%%-_Rv?oQ+J+2A0XduD3tbTMwumZ;T%JDNb|+ing}FNbj9t~
zYGxl7j3TfT+7h#O8vy*@Fq~5xnOT1>jYI=xJWjqnga#r=N9ytv{fvN2b{8`alWjGR
zxGp9OJ=YMcpx>2RD*S{iX1{ua$G_fF-G`KzuP(cV`XlqHAo&r7f6owqz}@^MOA{#l
z4KRTMsx;y;x}?Yp$|XFTGd=EXS28c9e09?>)%mkh%af}^xQtw8f2@dr7LZh@?Sq?>
zcW-rMFZvfi!!af2oBTEFEzu_^TzVv`3!l41E93Syt^yVFVj~8=LJ2f0!YqbD6YAk7
zKmYI0w$QC~$@pI|ANU3a#__+FLk|4sGU%$9UxpGmYm!ka>h~0!kQyrg7CF?}ro^aJ
zmM$&Bh_;6e_0pGtO6v>oyxjAmau&Zc6ua{CZ7e(q>9`2LS;159*^j)IQzPWhz;`GU
zSQbg2d79#U7UBnOiXWtF-y{&tWCj$`AfDkme-Ah^Uq^Pvn8HXAc8;&8f&=E{f6Wa-
z5m0=p;lR})#1J*jtIM;G5V4H*&_e`EX|Te(Bdh7$yW%)UbrRPWEnKA^LUWChkgd#q}YO&
z-pbQge_K3HLX{vY(v8Ndy#VD-l=A-7^=uxXfF$iZecnnss~ZngOBXAjT?%fNp=jA@
zJ$hVjBu#m=2~kpYLW_odtK3bm|tv16fZEfF7}7vKNtrxO>y&HXNY
zk@aEbvcNc!%FRn9e-n0v=&ZM~tIvl%zUWONu6EzU5^P=>J9d(xjqA&t-4RL^kT$9l
zs!&!tAx2x}F{d&--V5*q=Tp4jlGPnDEu6(X`YCrSOJRNsR_>@G$&QqRv*Wj?Cm3z1
z+B)G{0Tpehdc0unLyH^!<{~%!Q{=gk$$^+9v)6?MC%xlIu!lE;cR}zfui*qpu
zU^U+QL4`B4A|#i(N|ymR?a!s_^Ah%HmhZ7vH#H{U^TAxnUVzYX*gi{ZONznMsp>8G
zlXqmIR+hA;1|j(3Gmj_!Y9i{2*2{s$HMiU;=fA^~lna|G
zxh0n{QMbc&j`l3G^&pebs;Ioym)!V;h)pUY*1FX27P^te?Y!%E9}ie*`yK((+Qt;c
zOz*W3T1(fUGu(h0!oCiP`+vo+kYS(m;!bZAY%lHmZ{}&ABjSMEp6dA==9@c;=AyCB
z8OwPO@f*ZPn$4$P<42s$=c;(mxgY#To)~al#PN04wIJIxvGI~PN*cW*v1o!=EzemPx0zMa
zZ;bBC-;*cnZ5Fu(CV*q;^X=o^R6(neD;u2-MbsJ?Kjh~J;wxUx7rv7sMa6
zyXZ?tB}`;n(PPqEne_ZKK8veIPl?3xc=X=iHCs{s?(J;=^q2zSXfX0of1;|Y8-6~E
z0M@h~)kmZj8PSo0-SNBm`LprhHawiDmwzvb2zgeBF8{!X^8suvETN+W_L=@4d4A7W
zmL_iFGYhIs30Q{ZoSWb6&XY11zMGy$g_^c`Ov>t1n{1aP5GW8ogd;NGaULmfMu9$U
zn5j>t{)SjQJ1+Pv?+z~;{rmxa-^X3hY#TYbVk%`~;i=8x^iVpcOtAVRkk1PCE5}rj
zt5jc=%`1}Gj}eF_ZP1&r$h2X$*+^*FdG3x&Gi4V-CsNcM+rCV8VyVMXNF&onDL7xn
zm~~o?EWwUaEl48ZzDytdEG(h2YrjkwL#z^Apg=RlSF1_HqQhlN_Tu<^R!wgZ19c{V
z!-Z~!9%J9k7vj3rc<76Wpe8%K$#2J_8wXpU6c-!0ObhVtB9GoK`}`z}t!-4)Pw>RM
zRrO<3PDYzdenBPA`qhZcPNhL=bAxoLm+tI^15f7^8m8KqSoBc7ah`}LWWEl$;5w|Z
z!Fx2Q9nGe0=oHdN$Dh=U_D!5*+(Q=AF8$albswx3DM9U%mt9ui3x8Vjn427Oh
z<0Ww@!X21VEnjhmXtAxo*TzB>OL5f~);4jMi>wlV*nG6$5a4F#!a{oYr-{P633WH8
zOo-HD6*7Z>P`;2g|F=5pqqDjg{zlHLhxp4*3W>jE;t$s)8wQzC{a5al8z=UxphGwIEah$cFjbEH#H{9_a9S-93G65cv3RM3dFTa!q6L_9(KzDb
zR4D*OJ-W&f98>?9*_xEntwV~W_#QtXHeUp4%z+|N4rz{$f!Ho3>#x|1Fw8Q
z%=fgQR!p;CNSfpCY2p~9K;&t9EhPUP851Bk
zAxxcpgugdR!_lo^8@F4?eV}dX(t=nzMgzQJD$PJUti3p`atbkJvzpu7M2?jRl)Gpg
z`Mt!Bv6()f;+<$nKsW1Fg*r-L#@jo%1>343`}n$_$F&I53*~e1H%jhb}{hp6wi5
z)y#9ywA5Z|$1rl|UveX7vc172mnXL09h4=_n|rk7WCmIj+TT{{hk-
zJnV~qI@rH+1`7AlIdqexY%9jF
z)q(f5rmv4Yxp^EzJjov|oph-da{!Yt_AAPS$BncKzSe_>+zr%w02^c^eL7W%OPO$*
zIxc*nR2bh<^zNxhC%<{96w8ukobU|E!i#DkA~ALjvWNxaJTti7(fDhL%#7~3WY{lJ
zo;a49@!Zfk;~wUYVtU9PNGs~?_p6uq)d%SD1B2auw;*cYGSQmKfW@YZNZmR;4Jx`{h%yy)dYQr
zt@w6Sex+QF4u@e!9ym`89{(vWzH`&Vt=BnGZA8?Vl!`Iho3K=WF)bNpvza!9Zl5FAhzk;2?O~IOhJz<5C8nJx!boh5
zeRIU;CDx{3AT@eh@*O#VXla?V2=LBc8ls1(3V;3iTf-7)j^(bo?j#`WGJQJ1*h%Zx
zR1(z_#qZ}b`
z_j*zU3xpSIr`jU`rv4;!#F#3Ic28Ex?YG?cdl~o~OsS0ed2`_93i95wyaqr-xTQ1F
zi-iZmY3XQQn#J~Uf8ur_&~4m9I=g$(Z?Ju{9V(Y}|C=9y47Xv4p|vcfMt38s;=AcR
zOdh;-S~GdvzW^pn#99R8FWMGoD6qQ*@I_
zHlQZ@RhZSv-X{dsxwIrHRCz`ui+7lbs@cD{C_VlgiT^e~*;|O}1<wPnjA&`|P)rr>99aZ=5x4*D#;(U-K6`Ir
zSOW`9F0mTS&-_LSviyZE1#Z>CDqwmO<|7sYp-M#Q0ScV_-$-%W%L0=Ave6)o@9Bk(
zWNA)C<>JD8UmEQTIK~eNt)lkg=D6hJ_$}O{^@(;WwLXKRS
zqNbV>!OFaoo@j?WLF|YU}0P}K=ani9qJHOnzwAt=SpT=*PFXmu!
z@>E_*KCrDO2tO=SZ>=3aRZ3}CS(!g`S6py=36!ikbO&j_rE=8Wb=h$b&2!E!UAvc^
zm#;Q&`ua*bYL41mc`3ifN8b^p^?xtOF3*YR$jA^-9>dbhD1R&{r(#+7c0I{S5g
z=KQz3NcG#+4rF>_tB~gFEW2c7yy2-9U}?L#=%44Cv*dAs;L)gw247*jb%W{n{8wg4
zscFt|SL*$
z2!y5c!8O>CSr?+T66REewdMc8fhWNc!Rm*(%x{a!32+ltu{XP_DXFe%&Yu`?t-NCNZ+qV9}-dF%ibhW-Soz?`vjqUhmlsD=_h5QZ*5NSf23
z65X)`bqx_5`3}McHHQVJ3&nB5x9%y=Em$X-!kxXqnMmRyS%uPx^e1Fv$;y=HCaMyq*Sl87b+d6}O1Nl@%
z=bYi3;Uwi1%k;})v8!lR&D#NCUJMV=Vf~f!G4KJhMJx;+YC1E_BD07qEEA*27bo3#
zxDA-UAzyx(BtWMeD>RAeQ@|VMg10YYn!9}dfc}NZ1)?AVtyD(ONh1$zqX;A5+U1w;
z3?tcY4%;}5Un9Ri9j?V2k7Hi-taB>QMXbc
zn*=$+py&qwtsNaePb6_b7%vDY4^0tSDGkb~C$*jdex$S>WlelM8T4xcn1E{ogkS@eKF9RDdr
z!(#S($E?h#bMf@hY`cybuYL(a5Ul|nsxKj)^yPymlw^SYsN@^q6Rx5}KV^#dL?F`Y
zRg@ZEsPd+YYfc*nqk@f6%o_UhZ!k=Hka@OIP$(GuwdR9CA!Etf89q7BHxg?bl*7wc
z{10^B53n3#Ddppdu-pa~nV*NqP?4`#Z<_100^2fF>?+3eOSsSvo~n=)R*8c3gm6%@
z{}uM3J7sdtlrk9T+8`K1+qjA=yt3_9vj36Gkn2DA+TQX_$DYIb?l*a}{jnLd`JZD@
z02+8N)RwW>uK;Kl5HE{5*Jx5h<%^)f>xch;04K(x@3T}75BytBOP18+~=(K$L_!W=YNW`AE!kT
z;I%`-C#H~$PRZN7i3B-0nB4KP0Cp)AVG`O>dG{_jMuR0imc8f=X35&qK1hGz4%!snx>1ehns-T$;(Ra~dbQoHeA_HbaKh9FN9am&FQFo%Xe&CVI;tzU^C{ft;na
zLBGpdTXX27IT6dZN^`nfB=_sHH((L+RP56EFQ`cD%2(R_px^7XVte}=#kt$+JE
zo-0ELBc_m%r;S!tLHULc_jJ&yUQ3j>;n{Mw9DR1_DYZ7`;{RmP0m-W3@^+ri=)XyA
z$hHfna0MQg$_)mTHoP0JrIZR@=#zAWuV#oiq9vp1a$DX`!uTu68@SVOE5xe~3I6?6
zwoMv2oM!mx_!MK{Lwa(8rEOT|imtU55ndAPun8V7@XCBw1WCxnRD+sf_5A5GT@Brl
zUg|~s?Wou9#L{udfOoZQhU8EMWp45fm@dDiuiTJr(6sxk2SvC0O(VAD&b{wLXBD4q
z&az{kY@#)or8I}*R`$7s-egp5eW;*YLRx!C_GzhsLw07YNXt$vzE*VMauu(*mcmd4
zmOvyM^pRo0qA?t$Xr7E<5?u9q7XkQ?(
zYG2z&Vese$XbawJ{M;i~%CucV{AKDjL;~7wPDm=Gx#5TVseJ?Ut~!|Vk`gR@#3Eq;
zkr`U4#o#zntvFq!l+$rBX(v}`H(sp70TWjY(v{4H1G2GcMBDREz4N!Kw3+%)c%{i!h*p(&{7sNpJvXEtDDke+v+
zY_FQ1k#1x_SHxv!Uww2^KME;}pMlhxMrpVd}5U^`LCYO%}FbsToEL*RYo;N8`n(dSDq1I3tUMO@~a
z(@B@qY*%b}eL^?ID4oo|a&RVDKiaMKf@ZT3$eJock;T-Kt-l?BT=3xT|q@lFWbbHS_56z5n)Bch5eqJpxnbtzY
zVs9D;HPw@Qb666^N#V;H8D6P&IeQ*Gx!~N5;BoG3CWRia%$h`fzR6$2Q+|uTLf3qO
zcFSj~_2h&Xc{&g;G=a|G*w;V2tLS1#&tyhUB{(f1!_t#KlKm9D3>ESO2UHqM8A=Ef
zLQo9!FLY2UKdH8sLME=x6_1}D7~TAQxfi&L69V~f{12Tf7Qm)RRRKf84_pbuVce-d
z_~ZLE2>-_S8xUZ|P%9B!+htA|Aj1)${`^yO0r-+7YH@tp$8p5twc;?~&{?(LrU1
zO$xz&eKZq6%RAlBw+mtk-Ea4^Vt+}bySUZAXBv0?$VSADU+T%w3cxeqihg{=(}*w5
z!iHk;C5WMR0a*`2VJDDF7_L+;>4<$`;e|#8+7{5X-U-QkV%+@WTG|#4vNW6qq}c>&
z;HE1SY;GeybXCnDw5?|O~ws%h9
zTcL)6*gKU>Fmpg2eTAo%l~g*VrQxZeAsz~I*|o(kE)Z=2G@txgX@nDn%ptz3(!!e#
z6HcihI|AkX_H>b?GuWsHMvDU=jiIlKh2N1`C3Czznu$EDrUG^-D3?g+PFfH;6y-GB
zqRO5ru7^^{!hWLhGL=_60Go+Vaol48mz3Q
z^qA}=JXt?(gbyvd82FIn2rlJ`{g3m|^`N%+BEDwEx+jrOlK-1ptRp5<`a}FTr}rNU1pl7_E`S*pkacqRFm-Scx3M(0{~v^r
zmTIVsA&MEkXWL=ey(7jHNLuVKuTQTJpN%?-D;rBK$-=65cH?xuV%zM3&wId7w?+_|O6p*gRmO4r*v=cWXsJ0ccK=*WD>+833#iZTs#T!E
zs7%whGkVZp^I3n}vjaISpmwqQrrqH0zai`O86%C;DWnEFXzE%NVrQ-}>#)=?Bm9+x
zcKm-D7PXhlqZeL|%0AAo`85Wd4u7>ePbUO=fy%X6g^R$gb~@AbiTrDq%s;m@N;|fK
zmYLTfh&I(?R{9ahnuO)S2QOF$yfE?W){$23*SKo@Oim=u_g3qvgPJr5HKXL>WPX;N
z7Lr2PJwKA691y|Jgz>ElIpH=5@jX7FsOC1+0zAK4F0R|Q3hGZZ??ASblTkYzrbnq7
z0PLpZmO~wXeE%*k;ou`ypa!WmR_;nfZyjj~##gusHhez1DR
zqjpA3d=npHwp7I*uY8vYe8tr3cZojB0FbH0sRqi6n(!#s8KpLI#b%+tD;y#hTA|M_
zD{v7MkqEvv&bZ_M?$h{WXx*D{Q=TuT@gUng@@yKnr-#}r0T7dp+0%&!IW&=cv?gMb
zuGVFZ=Z*w(ajmE#M%*)hl2WsOpg1)8fX6_NEYw6@dwcaVe8x{$9;TwRcyjetFG!SMDs#8nqkHnj&
zm<~xPxe>|!{c)G*Q8;PcaU6aDNvWm|a$ek`Lvp$7i$i*qKE%7y`9`&C%h(n~uiyZG
zskwEc-K*hZE7Un?x9rv_ZjY$}2kP8EP&tw7E)3rov-H?-(!5$}-WM5XFUjV#j}yr=5q6egj--@?H(CQu=6@
z)H6!6r_))WZ`Q92)G&69pcb1`3i^o}C~`E-(JvsAK5sNck_tzHZYfMy$~}T)xY#?W
zZS#&6*I=fm&6
z>UNR;)sCb99fw1Zfv>4bv8%h{pr7P(YF7^D33q_g;f=eHinkx2@M%-rvecSs#X(&=
zTdg#0laQ?`n7**%sHYichsq9l6_xM9VcN?6%ZtK6CxbXcvm2?W<{SB#Uda#$sNV`@
z>f*@c*tv9!DNjz4|Mi$usk^jlMV*op+gW5$<94J148fV48e>FBU$!Y+(}58BcJ)$H
zVhp=OCiOFHxU;A^r4Fss=~wOawh$4cVbC3=JR(dbkNJ1b+j_`vwiVXWh>XSGOmZyo
z+q;;PTeGyf>>8IqLq$YMv#FNAdXj{{XVuYzOtG8;dA-dvku|-brPh2U(X@WjYO23;
zN3jA1(Ua>^{bqj~IAvHDTKojm6iR>)+$Fe^E*7t(4OiRi5#z-9|jZ9c!Aa|&I{qM>0Rr(JA>&WkKCN-QZ
z3uKKmTZYre=imJnNP?XCmxDoUP?L-iqKgjlx@bKOb{O+;HuW(c*|G$^0z?oYLzmS^
zw|`UP(iAAD7gjf6t_j))Igl@j;4;hOlB%_2$>W{c-RdLP*%4nty-CmBXeiJk>K_eqEFle
zEl#OaykO)Dq$pfOZcmGW2T$u@Y5}{$>?E@W!@Aq?h!us126P6xSwo}mT1_eR@e`|N
z@k{$qCBKyLRH4&cCncur*fm9Bx&3;6acwzhQv_9p$X4QejjPuKe}qI4WN5C4Wvdq`
zbV_*_@whKj!$xuPLf3HZ!DwZd>aU@n9N6};m!c(;Wuw4G_HCS0IFuWCn6|EeOgZe?
z;a@3zSKPdcO3fRs(en)$ipFcNgY8wN6uvokk|dvFJHcikv+d%-isH*{j9SDqhqD+V
zL_^MLQSITo060qkvUsXG4er={`R{|^YKG+4?1z!UL=tceM4tG@2q{v@{1mPZ=JPA+
zYTXESRLP3rV9o|Tc$`!_ddyGYMd=DvSI}yQ4D+kdo{Sg+LgpR%`8QyH@jvjHl}4YX
z3U9OOUDGeX3-CJX`fD*#gV@^Ob!&~JDC-6xHweiFlTDie-U{RIC5_Rr&Cza|E92^H
z>^Yl)a*WPBbpK-7xl`z4#_IoyBnuba(txkDOL!YAm7D459A*!0Te=s1YXMkG^d`xqC?6-o0^YiK5~QMaLQczA9`L$jQgZosC@1X9JVtyT<9
zUVC>Yk%JcAZd8;4bic}khi@$L+PU|GUmkHGjHhpw(ZadkL!*-RytKy~YJg5fApZP0
zem^oofz}FrO8we7eYai(gKfbW_t`t$Zo_@Wt5h5yOhE$U(I4f!`r6{pZa2{(^3Tll
zi8s&rK)*<=K0NaI1c@_^*59K)PB@`(j_4PhnahuQe||vpl;tkNYKgGt`!g)UDy)YL%}G%NjT6nDJ@O8hz6dV7o?bAc$IY2}I1GXrt@
z?=@4Ypkm82@CV8A>lQ1W_f=vu&0@KmAI}1Cz{R<3I?#3H9(^==i~VCOjoRuVtS46f
zmrIT9*l;`AMLId@HbzqqHum_+`9O5o74xu^c{onz>L)6WNO&0pymYe47W&2D@2l@r4mzkzc`!lDZ3e!+ox^e?CL~*ORHGP5Z0#zT2&dRU
zr|Giw%E6(9t3Zm%u$tji;!@tDrGB?kt(FmZj!PW<(-`8}J5fK{<1g0!_VPn7N-L`i
zRJiU46)Z&SJ^bnKZ2;CaivXqE+0^c?5<7_4h5w{4rxEnXPbBf6%LJdZGza
zyCMe_@(BJCGkXjZ!PW3FzMkUX3s>CVAL2448Q@BfR@@@+{hVO2eQ%y^xTyj7zLJ5k
z1L6vy<=3@$f;?dQr?~7NJ+$)&>(9Pf09E=k=_|GACbL=bbdB=yLw8%iy%mEiq4Ko+
zclp6KS<{#C2obPyPV%6f_cdk=0k53%-vRn+GCL7#Ik(zN2QwWJS0dujhbgW>L}MjnFelrnhW`3*o|5~4t-eY@qd
z>0JN)R`@`<#&1+uYk1Sv)2`tZtG06$&eVp(M>z4iSsX>_`+jvEd6S+x<*D{L!B|x<
zJiZl$G~6K)Muk+5dv_$TV(U%kFr972&kH|CTSXvW(8p8F)8yrJ49=gFBpyR~VZOtq
zRQHM8Mp2ovglp9^t_Q4ZzB~Nt*RgwYHyGu6ywBst+d#PR-JfK`o_^b4y0piDBOo*J
za26w5bs$J*BF?1zZB&vJT|(Q)g@2ZH70AF&NTnN)UOJarGNEjU^AiO32W`@oin%>C
z2J!TBXi|x@Zc>87G6(&-r2Kd+X5+%*-PO&uZMQ3W3I=Mt5)F{8pI&ZntXM#n$n(7O
z6K7<@8(PM@l^|@hT~4yHi<%CLiViQ;(Hr^YxqNe#xN0upuuQa$sNry8aaWuR#d(MA
znf>o~Xs!3yjmlfPye}krTihRd`(L(Xpqa4D(h0?^t>N5kq@HX!M2y8K+IvAaeHUNt
z={(JH6}5_Wb$DQTMpOSRbPdz(G5L&8SN^FeJDxYoS-$&+bv7U;Uq9>O=4G>?bIk1G
z=lJnH#i1pTkM*o4ATJ31o4)*&3|PqXt=BpTuLBbc^nYQ4=9{8BK@Dx%F}0i8-ic
zByFcQ&b(FPh3KOq935FTcx?9ef_$_+v=^^MVkzImGi8R;t`-8(4
zBYRTO@_AmO_gLFcd^eE3@@euY)=v11CiFdoqpXba80D3IiUFpwv7lT?M$$VzxdoFi
zJ;)u}qOKIL6*ZYf&CSV0YkI0H-KkJnl$@ll_yc&bb%9&_-i`M3XySwy5bhLi#a?)7
zeePbEEzf?A-TQj3HS=V4;+Pq7)LDYE7uOFa^@O9qFIS`(!qHde|HFy{q~&u@v(y2x
z(l6$`TgTDz{rI9Hi=j7cS3mqy5A6;FUvyj>BL1`bvSI^9w&7`7e&S0+QaDfdim23O
z8VvYV^#sy-LHHoMZrZX{6+#N@4f`x3;gNH%X-iyHwgx$u+>-4bOMY-TTTjp!j`BC$
z+z%GfSaiL5i%rOSaOEL@&z0dnKG3#Y6^gYIsnlR#qKTZEb^4&>$*Ss!u;G4>2VvJ0
zQCjJ0B%FSeQ^k0kSNc^{2!MtTka5K?LY0X}k!FV$r6){vnG1mclZRW15TV6ByNx
zsP1+2L5f(nz^tcaJXqQ31gvuYJFj8o7=jlxE@@!cXebr;fU$5YzCR<_KOFsIf1cw(
z^8%-nk;Dbn9vJszr%}3Kh`;j-e@Fa*`j=6|Lec<0OR|R__R8~Xt@5v8HtSH@2M3TC
zeuC=rCh_^*XMRls#KL64C!!87*ZZt)8&gsIN}EPhcE``A7Bu6|5Kb?R0{UAP27-!&
z&rVQVc@}N8!AwGA>{p*8?ax#`nh%8XHHM3OCfl$7hT2fHf-8uEy@Tjy5Q^HZbzVa`
zvso)Xn7Xp1y3U1Sz+CKiF0_6rpaTS=mKeQZk9k_^;`NZ2oAt;Z^D3Ff#VZOc-JA5G
zS%JX#c&uK@(lMo1G=&s6EwLb5OE>lD$hse>^$=T`w{#l~)Zx>)JA4+Jin~U&H?|>`
zqlZ@dMfEn&?~vvn
zt?eVYUdVVhwM}2ES}w>T3?nwIf6F!=>JXgwM$1%81aS%)XRweETO
z{}w3VGg7Q!Wfi8O#@ONle+Y+1Ss}~|Zh-$bldVWN{4#&&Y;hd;5lHnWzRoo(D6%^o
zqOq)IbQ2F=y)mK~qOo=Ov*3@O0QANFW3cZFVZHI5fXFE?$RF~K#|=;!2GvubB`BhbwiL_3(~Jt!=5NJG-b8}gp`#*Pp)v`M72u;IEg4pBH)7;IyWO^@&H56Z&<
z7aT=NKayHO*nc|-dG`P=Ein|-PsNoVx=bc*7_8l}IvbGA22#QU?=*wws!(UEpLDgWk}V>hc&i3-`scPPeoect
z59)7t{_aRN1w{oV&cXu!5Cv-nK2@+GQK}lHL=g}_#De-zD}4cGgePBksPIN7(j)Wt
z6(9W5W
zh4o(*#dXZ_J@Fmk)RIVQ<8KXJ7s1AsRJ>zr)O}EcOG`KjO|k2u`Vsm+!+N?do{3a1d&Q?oh&GX2#w=Sc@qzxkjYZo%Q}zH
zBzP$gte#v;LuhjDZ>?vNMt(8AWumrP;;hh&I>(RxF&6H0p9=p
zrVoMSx@hSbW8c-5-8smUlIfd?Rj#=}gsLGgZ$-68x;j{HZZkC)Kfk5oj}ZE$Q$2qH
zlcSSafoIFz&AftXSDMBl44>j0w)MPcxL8q;2Rpt~YyHOqul$oIU-$1_8x_ar4RFn44%w%P;yIVb9ef-7}0iV__Wz7o;!E>}S
zoaxaqaj|bsGnk?tcIg^)29X}^i-en1Xw%D%Chn#sDLmn(yMHKt*nH#;(v1O}gRE-l
zNj!FY8likgX^GzhdF$_Pav7>zSEK4^Oq6IB=)>RiH
zy!TV-XP=UVNTNWx2$mjn>zDzw@5aP%Z1iHpDd3blqoAL%<0{<
zefvLMTy<1bU)P2Kq`QYf>23s(mhKK|X^`#^7)qq;BGO1pcSuNgGo*A#gP9Si-|y|DEN(ofamDx=H@h3gP&^`Dxi~>F
zz;(*HaHsO^{ymGm>C`-PbmCl*U<$2KD(>SCDs?;V-Y?)(&IB9;1crx=Y0*(a=trGB
zD8&r1h`A!zN7y)b9-ZG)EkoQwz99`kIXxw5o+qNC#>iwx=e&{CsizuKDMZ+b6G`+rLLIRzc1f_leG8
zvqD@L%3a!qfE>%I+V(3_)000>pqyFwrV8;@V?rc~o@6-VbM)a&or~$h_7Rs&p&{Nn
zU5qF4=-FoP)rCp>is*&o#^naqYuT2GPG4q;ahjrWo}A={bB14z2)Qeqy)Zk9>PJ9po=#Q`NPHZ1QGo9&CYrSnF>Pou5!pH3>U
zyb5J_Zd5ytZW9+%frh3;j-mlQNS$=|m}TD4a+4qYsMRpOrAwr_S>H}xHOFTr!egG&
zn`F)6(XGYLuf@w(Ie)M-SjuCYX0a=7UuoMgtEqL=cKSN1zRPzheQ=Rgf0CPcRz&E!
zLMN`Bb`4T{<4AP87Z?@@tq4Pe6zB5qL2{q~@V4b*Qq{)`>A
z;ffhp7`u;5N%!hAMwso&U({Dk{c_gTt7j|tQdpn+b^#P7La#U~RA}W?P}6eHaQnt_
zczfTzMVMKf>e*kf92KYS8Ei38>S4ZDBqR>>Q1(*$%lA{}C6=4bf^D{?%|F6KKDSH~
zFbPV8neFNZlXl~;5*pP*HHR@%{UtiqjrbMMb5|xAPOw>!@WqIz@Q>-}N0kQ#?hxM^
zh9m5x;BbIrQ+0iSNT{k_%x`pZLT|Y~@(kirT5{W)*L{GuLLbYvrEnzM^3n1DPe8D)
z#g_VKgOw4psYwNtnWR(A*(>q@l~?kEmnfACCyM0lW_#MLG;7n)zns2(m-XSR1DEUp
zj2jm`+gz%oqUix@JLjJK(#EiK5Bu6$k?7JM@0082dXI3lc-^%m)_P1D9^-nC`H}*qm!av+;V-%t
z5|+zZiR$P^*t6j}r8liJ)}O0u>m0!^noOGU5At6iCcu>e+;qumP`rM%ce}a@DPO3u
z!M<}qX>QEaq1i4;i8G-)+7}CxitjM}hHGYONPB!>pQ9HH{^IH7yclB=Sqb#SS_=`t
zMtqj5O|emTcT(Yz7%9~xUBBg3TIf7~=6%e<%FWf%HWI0o3I
zYkbGNPMh@0+#>TzM4TFJ^7nn-YpTDQM7h#zlMCi_oaVjfR;^D{kEu!g}&Js96;>vsD4%
z!cTn2>BKDIi%+0YZ8
z7o^FZhM3qgy%geo7jSp?i@1YIhweG;l$@lN
z1SSoE8QGZ`+J!*a%VW&ZFUYanv8a$ug4UEIs&(pq+F0f%aaRiL$hlb1W%=a+Y1gof
zQPu<{;~2WLa(2C825n`%l9qe2+FHmgL&HgmfuR>8
z;EJWyl_SuWYCepitN9d)E(uhWr`4DiHYjV)2@qhF|M~7ItpHRRpE11HnscS&wEH?x
zV*5p(!62QB
zo9M_Uv*ah(3|I6^0-p+pxA12r^)tcJV!x(HyWn{m`kK6u_bexrGeoz13@Mr7TKWYB
zuk7Tpn8VhgCDr<7H6kiULt(Bwg>NG}Ye}(xd~+koOhazK|B;$8$n;*~&2t4kK`lws
zvjxj$^O7qx?T=ropoAcnoeVRcvn0=GEnmsOln>U5(vaclMwQS%4H}g%Ke)0v2-cJQ
zlu-7s)Tw(mcJYn|s*1$H-*oT6yF*su`OT8*{gbhg}e!%ab?AoKYMVjYC77z{yS}>qXrz!7P
z*Eu^B@Qn*J<5i-sxJ+P;6$M$(ve@);>QK8f9yhLbk#$(66%9J@iqs0qyM}D1JED7`
zgtiB%^l*VrzeQ5xoX$t$dz|t_nSMX&0*%Tyo}oU}DKAZeYp4A;LFmy@%7i!Yo6Q60
z2$X@kE^6W3#g=b1)l3N%%2QCSJt>m+i*U0`pSM*^G>)JkU3!w?3J}kHsV<0RgM9X(rx5W>+=Z-DdJ~cTk#jVgQ`zFmTp#~>xKR7|s7R#r_II{P020@S4?HU7r^wif
zJYiJ>2>`XJo(##S?xx^U$g{{%jQ$d}76wUZpGPbO_0m=o{U*O?B6pxiY-=E#ha(95UCF@a&(zwOsyIlw3*|vCXbr?pV@5{YN>6ZjA@4d>@zHpxtyH
z>QOY$^umFMsZm+8ajxWTTLthvmvg{dSCYu~wUFA8go-sA7E-dFyVfGJuqW2=)@7*a
zgu%OSyA#v~2EdiHTx{!IHwgb6-D~u%~l=xIcY{e$O~ZzYU8F
zV#0C&mAoZhHWgUKfDI?|OA(*ZDo$5Bi2Em_*7^T69%tD`|6F
zRf_dABa#a^1fD@grvvt$?z`$<{_W1L`_mo>{d(X2MUk?f#cWy#E~C*)gRkCdODrWm
z?aI}v++t9NJ5@%PC`KJGSLlg<6Z8kMRdQ3_rEhz(p9If}^n_zDY%ltZTLIdzUhyS4
zF?t;-!%6=Z6XO58^j*BdAkm`qs?3Hga#o($Ij=VYC;pHE?bOed^B%@;vhKL9%<_xQ
z!Dk<>-;ps%t17f_Xfda7h{{@!hH(DDV=s`+*VT6taYG_dTc!Q_13iCWo2i02#`diOuVZ{rd%|YCfJ6~3
z705b0heS>{H??J{8tM4@y(#~Wpo%xk-`JP+9oB~Zkl!5d%<2O%kLSMbes2oBur-zr
z|Mn)i3zJIacN5+97F*&p&N!N80-jWM>yt?oYZuhq?6D1V=0HxHJB`G9M3h?O_w68T
zzeA0&33$CA13m(R2r%hS2b_I?Ku2Hic@e@@irV-`^I?dJ2`thsQoD)nLBT>gcG6{a
z(&Z$q99V<#IQhIDR#U+g$1UNJa_Y{KE~LU5Woy1mxc6Z@moK~p_S<-Ydb9(5_@AF0k{nPi+zDx9Zh+c|KvNFv4NrY0Hmb9EM#ssaq(arJ_P@Z5!^ss2@
zdA2-|!DUk9n<@|kn+!NnJ?h;REO~9{OP@0`Esxnei#f&dX8K>trD#;L(@wOfW&?jP
zmV!U{_(*l-`Q4J4h#3blRvC2xO4muD@K<5lsbOjFw`98%=b$MG$WkkR}-(+VBE
z@}KulQU)b+468KIIj|>8K@B#T^9s7bkm(VrPp11XY#Z_xqZp@5nDPG5qp=BM7pqFn
z6Q4q=5F!|9xP#*5h9J6b9_ZtQ^_3EwNXThX2ZD&%+LW^zwhc8kcD4Lv_4!7$GgFoV
z9Lpas!19`IFn(@h;UB&Q_nA{87K(4YC~6ICQ^FP*oIeMI8M7W2LpNemQ%|w|K{+_A
zuVyoQnMC$FW19U-8@Q$8OE_373a+0ouKh$Hb4A5+)jkKqz})`j3_kb2HZX`7=*I_>
z7aSR3Aa&FEp0vgNER{;t|D{Lx#hY6G!#0ikT#h1$eW4_5ji&DptByD$@_4
zq$mM@?{^Gc4lRw1lkJU$hIx$jee}kLF)F%kovA)t=-Ucam^eAVDgEu7_L7pwFydqD
zAyG9ObHY=cY0?-@l5j$TWQTpOK<-~x=~9PLh5!`wBQGJI%wrhcXpLD_fkT*wy=
z+=_G!_sVM{jdFvH>0)$6FD;m>w(eqXXblCWp_Q<5F3_eC?-GjM7HM&eD1I
zs+wi3^G<3ngJdPjNr=ZlLs(2`mf8!w2C&%sT`TlT=J^nH6r)|ODpEV5)>uA*6}+bW
zFO4nO{W*ree!qt*;plg^20PFCJaaj!9+Of>`FmOz+DOzI<3-dOwTywYCW7+QjqZCh
zjCt-ec(}%M8h?4VX!M3kRPBV?;2vKzYs;hEkjSqK=bk8A{?bsKT}K!LXT7SUzc-Zdr}IX~(^WGTuqsS(XMhkBlB
zMb2@nwg!Q#aY@5(U(>Ag%!Jlv^{9!{Q=NUJ4f}eW()U|^>dTfrV
zH(u}SsY|W|dXpv!h^Mv3>AT=LY)HCC#tCDV`0wdq`c`4g0gk165Q#w)%soFOK_rJ4
z-rtcF<+7fK)yi^b)5igBT#^|)xtZ|IyI0Df$c~qJi=8?Eog_xhHP|rc9r5y
zwE8J#TVg=B%c)QR0d!5*rR%qDl3z{KuZHvu!^q98uTO`x#>NSQa2KnP>|8YCQ84jh
zGq)J$Mj6#P)|1=S-3TJR1lkF-Y#N`e8-15jVqTzR;{RPYcBD2EyDQUE7Iq998)xXA_>
z4zqx?_#Z%-!_Od(h>(xQ6n*gkf^y&jH^X?4|0OEGYrg+;22p7mt_rZ-(zhOU`)e*z#^b9^9M6qhZ3k9WdSAIJh&&LQlJF8e@s+BV@v>a=nkA%(*tPZ5MXo+
z2c+ZysM)Z>T^7(s58(N@5U9rka2YoOsd~dtf$qy0^gPXK~)g&q8zq=_22ttppo$aO6XXeu@V2pBF<+1O(wndEa6lK)Zny4|&y7U=UH_L+E6R5Ata3_$aS833vsw
z1)ZcnV8>z7pr2X5t2AanY+4+2mIDM$n}d)G9wN9iLLkH0$G1_KWJsQ>j};n6?p>kbBp_A`>G
WDWbsF$p{Gi@ZUasP|4|kdH)CXgbPdn
delta 19998
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
z