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 io.bitsquare.app.AppModule;
import io.bitsquare.btc.blockchain.BlockchainService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
@ -49,6 +50,7 @@ public class BitcoinModule extends AppModule {
bind(AddressEntryList.class).in(Singleton.class);
bind(TradeWalletService.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.net.HttpURLConnection;

View file

@ -1,4 +1,4 @@
package io.bitsquare.btc.http;
package io.bitsquare.btc.blockchain;
public class HttpException extends Exception {
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 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 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;
// TODO route over tor, support several providers
public class BlockrIOProvider implements BlockchainApiProvider {
private static final Logger log = LoggerFactory.getLogger(BlockrIOProvider.class);
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() {
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 {
Log.traceCall("transactionId=" + transactionId);
try {
return Coin.parseCoin(new JsonParser()
JsonObject data = new JsonParser()
.parse(httpClient.requestWithGET(transactionId))
.getAsJsonObject()
.get("data")
.getAsJsonObject()
.getAsJsonObject();
return Coin.parseCoin(data
.get("fee")
.getAsString());
} 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());
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.btc.BitcoinNetwork;
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.CurrencyUtil;
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("Biteasy", "https://www.biteasy.com/transactions/", "https://www.biteasy.com/addresses/")
));
private BlockchainApiProvider blockchainApiProvider;
public static List<String> getBtcDenominations() {
return BTC_DENOMINATIONS;
@ -156,8 +153,6 @@ public class Preferences implements Serializable {
defaultTradeCurrency = preferredTradeCurrency;
useTorForBitcoinJ = persisted.getUseTorForBitcoinJ();
blockchainApiProvider = persisted.getBlockchainApiProvider();
try {
setTxFeePerKB(persisted.getTxFeePerKB());
} catch (Exception e) {
@ -180,8 +175,6 @@ public class Preferences implements Serializable {
preferredLocale = getDefaultLocale();
preferredTradeCurrency = getDefaultTradeCurrency();
blockchainApiProvider = new BlockrIOProvider();
storage.queueUpForSave();
}
@ -304,10 +297,6 @@ public class Preferences implements Serializable {
storage.queueUpForSave();
}
public void setBlockchainApiProvider(BlockchainApiProvider blockchainApiProvider) {
this.blockchainApiProvider = blockchainApiProvider;
storage.queueUpForSave();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@ -428,8 +417,4 @@ public class Preferences implements Serializable {
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;
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.arbitration.Arbitrator;
import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.TradeWalletService;
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.common.UserThread;
import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.gui.common.model.ActivatableDataModel;
import io.bitsquare.gui.popups.Popup;
import io.bitsquare.gui.popups.WalletPasswordPopup;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.locale.Country;
@ -50,7 +54,6 @@ import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
@ -70,6 +73,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
private final KeyRing keyRing;
private final P2PService p2PService;
private final WalletPasswordPopup walletPasswordPopup;
private BlockchainService blockchainService;
private final BSFormatter formatter;
private final String offerId;
private final AddressEntry addressEntry;
@ -99,7 +103,6 @@ class CreateOfferDataModel extends ActivatableDataModel {
final ObservableList<PaymentAccount> paymentAccounts = FXCollections.observableArrayList();
private PaymentAccount paymentAccount;
private int retryRequestFeeCounter = 0;
///////////////////////////////////////////////////////////////////////////////////////////
@ -109,7 +112,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
@Inject
CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService, TradeWalletService tradeWalletService,
Preferences preferences, User user, KeyRing keyRing, P2PService p2PService,
WalletPasswordPopup walletPasswordPopup, BSFormatter formatter) {
WalletPasswordPopup walletPasswordPopup, BlockchainService blockchainService, BSFormatter formatter) {
this.openOfferManager = openOfferManager;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
@ -118,6 +121,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
this.keyRing = keyRing;
this.p2PService = p2PService;
this.walletPasswordPopup = walletPasswordPopup;
this.blockchainService = blockchainService;
this.formatter = formatter;
offerId = UUID.randomUUID().toString();
@ -164,9 +168,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
}
boolean isFeeFromFundingTxSufficient() {
// if fee was never set because of api provider not available we check with default value and return true
return feeFromFundingTxProperty.get().equals(Coin.NEGATIVE_SATOSHI) ||
feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinFundingFee()) >= 0;
return feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinFundingFee()) >= 0;
}
private void addListeners() {
@ -205,17 +207,18 @@ class CreateOfferDataModel extends ActivatableDataModel {
}
private void requestFeeFromBlockchain(String transactionId) {
try {
feeFromFundingTxProperty.set(preferences.getBlockchainApiProvider().getFee(transactionId));
} catch (IOException | HttpException e) {
log.warn("Could not get fee from block explorer" + e);
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);
SettableFuture<Coin> future = blockchainService.requestFeeFromBlockchain(transactionId);
Futures.addCallback(future, new FutureCallback<Coin>() {
public void onSuccess(Coin fee) {
UserThread.execute(() -> feeFromFundingTxProperty.set(fee));
}
}
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());
}
});
}