Ad more blockchain providers, use try-again-on-failure

This commit is contained in:
Manfred Karrer 2016-02-08 22:42:52 +01:00
parent 1f8b1b0e01
commit 96090b71ad
12 changed files with 300 additions and 46 deletions

View file

@ -19,6 +19,7 @@ package io.bitsquare.btc;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import io.bitsquare.app.AppModule; import io.bitsquare.app.AppModule;
import io.bitsquare.btc.blockchain.BlockchainService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@ -49,6 +50,7 @@ public class BitcoinModule extends AppModule {
bind(AddressEntryList.class).in(Singleton.class); bind(AddressEntryList.class).in(Singleton.class);
bind(TradeWalletService.class).in(Singleton.class); bind(TradeWalletService.class).in(Singleton.class);
bind(WalletService.class).in(Singleton.class); bind(WalletService.class).in(Singleton.class);
bind(BlockchainService.class).in(Singleton.class);
} }
} }

View file

@ -0,0 +1,54 @@
package io.bitsquare.btc.blockchain;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.google.inject.Inject;
import io.bitsquare.btc.blockchain.providers.BlockTrailProvider;
import io.bitsquare.btc.blockchain.providers.BlockchainApiProvider;
import io.bitsquare.btc.blockchain.providers.BlockrIOProvider;
import io.bitsquare.btc.blockchain.providers.TradeBlockProvider;
import org.bitcoinj.core.Coin;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
public class BlockchainService {
private static final Logger log = LoggerFactory.getLogger(BlockchainService.class);
private final ArrayList<BlockchainApiProvider> providers;
@Inject
public BlockchainService() {
providers = new ArrayList<>(Arrays.asList(new BlockrIOProvider(), new BlockTrailProvider(), new TradeBlockProvider()));
}
public SettableFuture<Coin> requestFeeFromBlockchain(String transactionId) {
log.debug("Request fee from providers");
long startTime = System.currentTimeMillis();
final SettableFuture<Coin> resultFuture = SettableFuture.create();
for (BlockchainApiProvider provider : providers) {
GetFeeRequest getFeeRequest = new GetFeeRequest();
SettableFuture<Coin> future = getFeeRequest.requestFee(transactionId, provider);
Futures.addCallback(future, new FutureCallback<Coin>() {
public void onSuccess(Coin fee) {
if (!resultFuture.isDone()) {
log.info("Request fee from providers done after {} ms.", (System.currentTimeMillis() - startTime));
resultFuture.set(fee);
}
}
public void onFailure(@NotNull Throwable throwable) {
if (!resultFuture.isDone()) {
log.warn("Could not get the fee from any provider after repeated requests.");
resultFuture.setException(throwable);
}
}
});
}
return resultFuture;
}
}

View file

@ -0,0 +1,77 @@
package io.bitsquare.btc.blockchain;
import com.google.common.util.concurrent.*;
import io.bitsquare.btc.blockchain.providers.BlockchainApiProvider;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Utilities;
import org.bitcoinj.core.Coin;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Timer;
public class GetFeeRequest {
private static final Logger log = LoggerFactory.getLogger(GetFeeRequest.class);
private final ListeningExecutorService executorService;
private Timer timer;
private int faults;
public GetFeeRequest() {
executorService = Utilities.getListeningExecutorService("GetFeeRequest", 5, 10, 120L);
}
public SettableFuture<Coin> requestFee(String transactionId, BlockchainApiProvider provider) {
final SettableFuture<Coin> resultFuture = SettableFuture.create();
return requestFee(transactionId, provider, resultFuture);
}
private SettableFuture<Coin> requestFee(String transactionId, BlockchainApiProvider provider, SettableFuture<Coin> resultFuture) {
ListenableFuture<Coin> future = executorService.submit(() -> {
Thread.currentThread().setName("requestFee-" + provider.toString());
try {
return provider.getFee(transactionId);
} catch (IOException | HttpException e) {
log.warn("Fee request failed for tx {} from provider {}\n error={}",
transactionId, provider, e.getMessage());
throw e;
}
});
Futures.addCallback(future, new FutureCallback<Coin>() {
public void onSuccess(Coin fee) {
log.info("Received fee of {}\nfor tx {}\nfrom provider {}", fee.toFriendlyString(), transactionId, provider);
resultFuture.set(fee);
}
public void onFailure(@NotNull Throwable throwable) {
if (timer == null) {
timer = UserThread.runAfter(() -> {
stopTimer();
faults++;
if (!resultFuture.isDone()) {
if (faults < 4) {
requestFee(transactionId, provider, resultFuture);
} else {
resultFuture.setException(throwable);
}
} else {
log.debug("Got an error after a successful result. " +
"That might happen when we get a delayed response from a timer request.");
}
}, 1 + faults);
} else {
log.warn("Timer was not null");
}
}
});
return resultFuture;
}
private void stopTimer() {
timer.cancel();
timer = null;
}
}

View file

@ -1,4 +1,4 @@
package io.bitsquare.btc.http; package io.bitsquare.btc.blockchain;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;

View file

@ -1,4 +1,4 @@
package io.bitsquare.btc.http; package io.bitsquare.btc.blockchain;
public class HttpException extends Exception { public class HttpException extends Exception {
public HttpException(String message) { public HttpException(String message) {

View file

@ -0,0 +1,45 @@
package io.bitsquare.btc.blockchain.providers;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.bitsquare.app.Log;
import io.bitsquare.btc.blockchain.HttpClient;
import io.bitsquare.btc.blockchain.HttpException;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class BlockTrailProvider implements BlockchainApiProvider {
private static final Logger log = LoggerFactory.getLogger(BlockTrailProvider.class);
private final HttpClient httpClient;
public BlockTrailProvider() {
httpClient = new HttpClient("https://www.blocktrail.com/BTC/json/blockchain/tx/");
}
@Override
public Coin getFee(String transactionId) throws IOException, HttpException {
Log.traceCall("transactionId=" + transactionId);
try {
JsonObject asJsonObject = new JsonParser()
.parse(httpClient.requestWithGET(transactionId))
.getAsJsonObject();
return Coin.valueOf(asJsonObject
.get("fee")
.getAsLong());
} catch (IOException | HttpException e) {
log.debug("Error at requesting transaction data from block explorer " + httpClient + "\n" +
"Error =" + e.getMessage());
throw e;
}
}
@Override
public String toString() {
return "BlockTrailProvider{" +
'}';
}
}

View file

@ -1,5 +1,6 @@
package io.bitsquare.btc.http; package io.bitsquare.btc.blockchain.providers;
import io.bitsquare.btc.blockchain.HttpException;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import java.io.IOException; import java.io.IOException;

View file

@ -1,25 +1,21 @@
package io.bitsquare.btc.http; package io.bitsquare.btc.blockchain.providers;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import io.bitsquare.app.Log; import io.bitsquare.app.Log;
import io.bitsquare.btc.blockchain.HttpClient;
import io.bitsquare.btc.blockchain.HttpException;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
// TODO route over tor, support several providers
public class BlockrIOProvider implements BlockchainApiProvider { public class BlockrIOProvider implements BlockchainApiProvider {
private static final Logger log = LoggerFactory.getLogger(BlockrIOProvider.class); private static final Logger log = LoggerFactory.getLogger(BlockrIOProvider.class);
private final HttpClient httpClient; private final HttpClient httpClient;
public static void main(String[] args) throws HttpException, IOException {
Coin fee = new BlockrIOProvider()
.getFee("df67414652722d38b43dcbcac6927c97626a65bd4e76a2e2787e22948a7c5c47");
log.debug("fee " + fee.toFriendlyString());
}
public BlockrIOProvider() { public BlockrIOProvider() {
httpClient = new HttpClient("https://btc.blockr.io/api/v1/tx/info/"); httpClient = new HttpClient("https://btc.blockr.io/api/v1/tx/info/");
} }
@ -28,17 +24,24 @@ public class BlockrIOProvider implements BlockchainApiProvider {
public Coin getFee(String transactionId) throws IOException, HttpException { public Coin getFee(String transactionId) throws IOException, HttpException {
Log.traceCall("transactionId=" + transactionId); Log.traceCall("transactionId=" + transactionId);
try { try {
return Coin.parseCoin(new JsonParser() JsonObject data = new JsonParser()
.parse(httpClient.requestWithGET(transactionId)) .parse(httpClient.requestWithGET(transactionId))
.getAsJsonObject() .getAsJsonObject()
.get("data") .get("data")
.getAsJsonObject() .getAsJsonObject();
return Coin.parseCoin(data
.get("fee") .get("fee")
.getAsString()); .getAsString());
} catch (IOException | HttpException e) { } catch (IOException | HttpException e) {
log.warn("Error at requesting transaction data from block explorer " + httpClient + "\n" + log.debug("Error at requesting transaction data from block explorer " + httpClient + "\n" +
"Error =" + e.getMessage()); "Error =" + e.getMessage());
throw e; throw e;
} }
} }
@Override
public String toString() {
return "BlockrIOProvider{" +
'}';
}
} }

View file

@ -0,0 +1,47 @@
package io.bitsquare.btc.blockchain.providers;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.bitsquare.app.Log;
import io.bitsquare.btc.blockchain.HttpClient;
import io.bitsquare.btc.blockchain.HttpException;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class TradeBlockProvider implements BlockchainApiProvider {
private static final Logger log = LoggerFactory.getLogger(TradeBlockProvider.class);
private final HttpClient httpClient;
public TradeBlockProvider() {
httpClient = new HttpClient("https://tradeblock.com/api/blockchain/tx/");
}
@Override
public Coin getFee(String transactionId) throws IOException, HttpException {
Log.traceCall("transactionId=" + transactionId);
try {
JsonObject asJsonObject = new JsonParser()
.parse(httpClient.requestWithGET(transactionId))
.getAsJsonObject();
return Coin.valueOf(asJsonObject
.get("data")
.getAsJsonObject()
.get("fee")
.getAsLong());
} catch (IOException | HttpException e) {
log.debug("Error at requesting transaction data from block explorer " + httpClient + "\n" +
"Error =" + e.getMessage());
throw e;
}
}
@Override
public String toString() {
return "TradeBlockProvider{" +
'}';
}
}

View file

@ -21,8 +21,6 @@ import io.bitsquare.app.BitsquareEnvironment;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.BitcoinNetwork; import io.bitsquare.btc.BitcoinNetwork;
import io.bitsquare.btc.FeePolicy; import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.http.BlockchainApiProvider;
import io.bitsquare.btc.http.BlockrIOProvider;
import io.bitsquare.locale.CountryUtil; import io.bitsquare.locale.CountryUtil;
import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.locale.TradeCurrency; import io.bitsquare.locale.TradeCurrency;
@ -67,7 +65,6 @@ public class Preferences implements Serializable {
new BlockChainExplorer("Blockr.io", "https://btc.blockr.io/tx/info/", "https://btc.blockr.io/address/info/"), new BlockChainExplorer("Blockr.io", "https://btc.blockr.io/tx/info/", "https://btc.blockr.io/address/info/"),
new BlockChainExplorer("Biteasy", "https://www.biteasy.com/transactions/", "https://www.biteasy.com/addresses/") new BlockChainExplorer("Biteasy", "https://www.biteasy.com/transactions/", "https://www.biteasy.com/addresses/")
)); ));
private BlockchainApiProvider blockchainApiProvider;
public static List<String> getBtcDenominations() { public static List<String> getBtcDenominations() {
return BTC_DENOMINATIONS; return BTC_DENOMINATIONS;
@ -156,8 +153,6 @@ public class Preferences implements Serializable {
defaultTradeCurrency = preferredTradeCurrency; defaultTradeCurrency = preferredTradeCurrency;
useTorForBitcoinJ = persisted.getUseTorForBitcoinJ(); useTorForBitcoinJ = persisted.getUseTorForBitcoinJ();
blockchainApiProvider = persisted.getBlockchainApiProvider();
try { try {
setTxFeePerKB(persisted.getTxFeePerKB()); setTxFeePerKB(persisted.getTxFeePerKB());
} catch (Exception e) { } catch (Exception e) {
@ -180,8 +175,6 @@ public class Preferences implements Serializable {
preferredLocale = getDefaultLocale(); preferredLocale = getDefaultLocale();
preferredTradeCurrency = getDefaultTradeCurrency(); preferredTradeCurrency = getDefaultTradeCurrency();
blockchainApiProvider = new BlockrIOProvider();
storage.queueUpForSave(); storage.queueUpForSave();
} }
@ -304,10 +297,6 @@ public class Preferences implements Serializable {
storage.queueUpForSave(); storage.queueUpForSave();
} }
public void setBlockchainApiProvider(BlockchainApiProvider blockchainApiProvider) {
this.blockchainApiProvider = blockchainApiProvider;
storage.queueUpForSave();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getter // Getter
@ -428,8 +417,4 @@ public class Preferences implements Serializable {
return useTorForBitcoinJ; return useTorForBitcoinJ;
} }
public BlockchainApiProvider getBlockchainApiProvider() {
return blockchainApiProvider;
}
} }

View file

@ -0,0 +1,37 @@
package io.bitsquare.btc.blockchain;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoinj.core.Coin;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static junit.framework.TestCase.assertTrue;
public class BlockchainServiceTest {
private static final Logger log = LoggerFactory.getLogger(BlockchainServiceTest.class);
@Test
public void testIsMinSpendableAmount() throws InterruptedException {
BlockchainService blockchainService = new BlockchainService();
// that tx has 0.001 BTC as fee
String transactionId = "38d176d0b1079b99fcb59859401d6b1679d2fa18fd8989d2c244b3682e52fce6";
SettableFuture<Coin> future = blockchainService.requestFeeFromBlockchain(transactionId);
Futures.addCallback(future, new FutureCallback<Coin>() {
public void onSuccess(Coin fee) {
log.debug(fee.toFriendlyString());
assertTrue(fee.equals(Coin.MILLICOIN));
}
public void onFailure(@NotNull Throwable throwable) {
log.error(throwable.getMessage());
}
});
Thread.sleep(5000);
}
}

View file

@ -17,17 +17,21 @@
package io.bitsquare.gui.main.offer.createoffer; package io.bitsquare.gui.main.offer.createoffer;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy; import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.http.HttpException; import io.bitsquare.btc.blockchain.BlockchainService;
import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.common.UserThread; import io.bitsquare.common.UserThread;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.gui.common.model.ActivatableDataModel; import io.bitsquare.gui.common.model.ActivatableDataModel;
import io.bitsquare.gui.popups.Popup;
import io.bitsquare.gui.popups.WalletPasswordPopup; import io.bitsquare.gui.popups.WalletPasswordPopup;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.locale.Country; import io.bitsquare.locale.Country;
@ -50,7 +54,6 @@ import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -70,6 +73,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
private final KeyRing keyRing; private final KeyRing keyRing;
private final P2PService p2PService; private final P2PService p2PService;
private final WalletPasswordPopup walletPasswordPopup; private final WalletPasswordPopup walletPasswordPopup;
private BlockchainService blockchainService;
private final BSFormatter formatter; private final BSFormatter formatter;
private final String offerId; private final String offerId;
private final AddressEntry addressEntry; private final AddressEntry addressEntry;
@ -99,7 +103,6 @@ class CreateOfferDataModel extends ActivatableDataModel {
final ObservableList<PaymentAccount> paymentAccounts = FXCollections.observableArrayList(); final ObservableList<PaymentAccount> paymentAccounts = FXCollections.observableArrayList();
private PaymentAccount paymentAccount; private PaymentAccount paymentAccount;
private int retryRequestFeeCounter = 0;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -109,7 +112,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
@Inject @Inject
CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService, TradeWalletService tradeWalletService, CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService, TradeWalletService tradeWalletService,
Preferences preferences, User user, KeyRing keyRing, P2PService p2PService, Preferences preferences, User user, KeyRing keyRing, P2PService p2PService,
WalletPasswordPopup walletPasswordPopup, BSFormatter formatter) { WalletPasswordPopup walletPasswordPopup, BlockchainService blockchainService, BSFormatter formatter) {
this.openOfferManager = openOfferManager; this.openOfferManager = openOfferManager;
this.walletService = walletService; this.walletService = walletService;
this.tradeWalletService = tradeWalletService; this.tradeWalletService = tradeWalletService;
@ -118,6 +121,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
this.keyRing = keyRing; this.keyRing = keyRing;
this.p2PService = p2PService; this.p2PService = p2PService;
this.walletPasswordPopup = walletPasswordPopup; this.walletPasswordPopup = walletPasswordPopup;
this.blockchainService = blockchainService;
this.formatter = formatter; this.formatter = formatter;
offerId = UUID.randomUUID().toString(); offerId = UUID.randomUUID().toString();
@ -164,9 +168,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
} }
boolean isFeeFromFundingTxSufficient() { boolean isFeeFromFundingTxSufficient() {
// if fee was never set because of api provider not available we check with default value and return true return feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinFundingFee()) >= 0;
return feeFromFundingTxProperty.get().equals(Coin.NEGATIVE_SATOSHI) ||
feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinFundingFee()) >= 0;
} }
private void addListeners() { private void addListeners() {
@ -205,17 +207,18 @@ class CreateOfferDataModel extends ActivatableDataModel {
} }
private void requestFeeFromBlockchain(String transactionId) { private void requestFeeFromBlockchain(String transactionId) {
try { SettableFuture<Coin> future = blockchainService.requestFeeFromBlockchain(transactionId);
feeFromFundingTxProperty.set(preferences.getBlockchainApiProvider().getFee(transactionId)); Futures.addCallback(future, new FutureCallback<Coin>() {
} catch (IOException | HttpException e) { public void onSuccess(Coin fee) {
log.warn("Could not get fee from block explorer" + e); UserThread.execute(() -> feeFromFundingTxProperty.set(fee));
if (retryRequestFeeCounter < 3) {
retryRequestFeeCounter++;
log.warn("We try again after 5 seconds");
// TODO if we have more providers, try another one
UserThread.runAfter(() -> requestFeeFromBlockchain(transactionId), 5);
} }
}
public void onFailure(@NotNull Throwable throwable) {
UserThread.execute(() -> new Popup()
.warning("We did not get a result for the mining fee used in the funding transaction.")
.show());
}
});
} }