remove XmrTxProofService

This commit is contained in:
woodser 2023-04-21 09:55:34 -04:00
parent 33147e1c7c
commit 37e812dead
38 changed files with 5 additions and 3737 deletions

View file

@ -17,7 +17,6 @@
package haveno.core.api;
import com.google.common.util.concurrent.FutureCallback;
import haveno.common.app.Version;
import haveno.common.config.Config;
import haveno.common.crypto.IncorrectPasswordException;
@ -296,14 +295,6 @@ public class CoreApi {
return walletsService.getFundingAddresses();
}
public void sendBtc(String address,
String amount,
String txFeeRate,
String memo,
FutureCallback<Transaction> callback) {
walletsService.sendBtc(address, amount, txFeeRate, memo, callback);
}
public Transaction getTransaction(String txId) {
return walletsService.getTransaction(txId);
}

View file

@ -20,7 +20,6 @@ package haveno.core.api;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.FutureCallback;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.core.api.model.AddressBalanceInfo;
@ -32,8 +31,6 @@ import haveno.core.user.Preferences;
import haveno.core.util.FormattingUtils;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.xmr.Balances;
import haveno.core.xmr.exceptions.AddressEntryException;
import haveno.core.xmr.exceptions.InsufficientFundsException;
import haveno.core.xmr.model.AddressEntry;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.BtcWalletService;
@ -44,7 +41,6 @@ import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroTxWallet;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
@ -57,7 +53,6 @@ import javax.inject.Named;
import javax.inject.Singleton;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@ -229,61 +224,6 @@ class CoreWalletsService {
.collect(Collectors.toList());
}
void sendBtc(String address,
String amount,
String txFeeRate,
String memo,
FutureCallback<Transaction> callback) {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
try {
Set<String> fromAddresses = btcWalletService.getAddressEntriesForAvailableBalanceStream()
.map(AddressEntry::getAddressString)
.collect(Collectors.toSet());
Coin receiverAmount = getValidTransferAmount(amount, btcFormatter);
Coin txFeePerVbyte = getTxFeeRateFromParamOrPreferenceOrFeeService(txFeeRate);
// TODO Support feeExcluded (or included), default is fee included.
// See WithdrawalView # onWithdraw (and refactor).
Transaction feeEstimationTransaction =
btcWalletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses,
receiverAmount,
txFeePerVbyte);
if (feeEstimationTransaction == null)
throw new IllegalStateException("could not estimate the transaction fee");
Coin dust = btcWalletService.getDust(feeEstimationTransaction);
Coin fee = feeEstimationTransaction.getFee().add(dust);
if (dust.isPositive()) {
fee = feeEstimationTransaction.getFee().add(dust);
log.info("Dust txo ({} sats) was detected, the dust amount has been added to the fee (was {}, now {})",
dust.value,
feeEstimationTransaction.getFee(),
fee.value);
}
log.info("Sending {} BTC to {} with tx fee of {} sats (fee rate {} sats/byte).",
amount,
address,
fee.value,
txFeePerVbyte.value);
btcWalletService.sendFundsForMultipleAddresses(fromAddresses,
address,
receiverAmount,
fee,
null,
tempAesKey,
memo.isEmpty() ? null : memo,
callback);
} catch (AddressEntryException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send btc from any addresses in wallet", ex);
} catch (InsufficientFundsException | InsufficientMoneyException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send btc due to insufficient funds", ex);
}
}
Transaction getTransaction(String txId) {
if (txId.length() != 64)
throw new IllegalArgumentException(format("%s is not a transaction id", txId));

View file

@ -47,7 +47,6 @@ import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.TradeManager;
import haveno.core.trade.failed.FailedTradesManager;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.trade.txproof.xmr.XmrTxProofService;
import haveno.core.user.User;
import haveno.core.xmr.Balances;
import haveno.network.p2p.P2PService;
@ -72,7 +71,6 @@ public class DomainInitialisation {
private final TradeManager tradeManager;
private final ClosedTradableManager closedTradableManager;
private final FailedTradesManager failedTradesManager;
private final XmrTxProofService xmrTxProofService;
private final OpenOfferManager openOfferManager;
private final Balances balances;
private final WalletAppSetup walletAppSetup;
@ -105,7 +103,6 @@ public class DomainInitialisation {
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
FailedTradesManager failedTradesManager,
XmrTxProofService xmrTxProofService,
OpenOfferManager openOfferManager,
Balances balances,
WalletAppSetup walletAppSetup,
@ -136,7 +133,6 @@ public class DomainInitialisation {
this.tradeManager = tradeManager;
this.closedTradableManager = closedTradableManager;
this.failedTradesManager = failedTradesManager;
this.xmrTxProofService = xmrTxProofService;
this.openOfferManager = openOfferManager;
this.balances = balances;
this.walletAppSetup = walletAppSetup;
@ -182,7 +178,6 @@ public class DomainInitialisation {
closedTradableManager.onAllServicesInitialized();
failedTradesManager.onAllServicesInitialized();
xmrTxProofService.onAllServicesInitialized();
openOfferManager.onAllServicesInitialized();

View file

@ -43,7 +43,6 @@ import haveno.core.setup.CoreSetup;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.trade.txproof.xmr.XmrTxProofService;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.BtcWalletService;
import haveno.core.xmr.wallet.XmrWalletService;
@ -343,7 +342,6 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
injector.getInstance(PriceFeedService.class).shutDown();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(XmrTxProofService.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown();
// shut down open offer manager

View file

@ -57,7 +57,6 @@ import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.BtcWalletService;
import haveno.core.xmr.wallet.WalletsManager;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
@ -248,8 +247,6 @@ public class HavenoSetup {
this.arbitrationManager = arbitrationManager;
HavenoUtils.havenoSetup = this;
MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode);
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -431,7 +431,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
if (feeEstimateTx != null) {
BigInteger feeEstimate = feeEstimateTx.getFee();
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue();
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee());
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
}

View file

@ -47,7 +47,6 @@ import haveno.core.trade.protocol.ProcessModelServiceProvider;
import haveno.core.trade.protocol.TradeListener;
import haveno.core.trade.protocol.TradePeer;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.txproof.AssetTxProofResult;
import haveno.core.util.VolumeUtil;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
@ -55,13 +54,11 @@ import haveno.network.p2p.AckMessage;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@ -427,18 +424,6 @@ public abstract class Trade implements Tradable, Model {
@Setter
private String counterCurrencyExtraData;
// Added at v1.3.8
// Generic tx proof result. We persist name if AssetTxProofResult enum. Other fields in the enum are not persisted
// as they are not very relevant as historical data (e.g. number of confirmations)
@Nullable
@Getter
private AssetTxProofResult assetTxProofResult;
// ObjectProperty with AssetTxProofResult does not notify changeListeners. Probably because AssetTxProofResult is
// an enum and enum does not support EqualsAndHashCode. Alternatively we could add a addListener and removeListener
// method and a listener interface, but the IntegerProperty seems to be less boilerplate.
@Getter
transient final private IntegerProperty assetTxProofResultUpdateProperty = new SimpleIntegerProperty();
// Added in XMR integration
private transient List<TradeListener> tradeListeners; // notified on fully validated trade messages
transient MoneroWalletListener depositTxListener;
@ -1342,11 +1327,6 @@ public abstract class Trade implements Tradable, Model {
errorMessageProperty.set(appendedErrorMessage);
}
public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) {
this.assetTxProofResult = assetTxProofResult;
assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@ -1996,7 +1976,6 @@ public abstract class Trade implements Tradable, Model {
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
return builder.build();
}
@ -2020,13 +1999,6 @@ public abstract class Trade implements Tradable, Model {
trade.setStartTime(proto.getStartTime());
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
AssetTxProofResult persistedAssetTxProofResult = ProtoUtil.enumFromProto(AssetTxProofResult.class, proto.getAssetTxProofResult());
// We do not want to show the user the last pending state when he starts up the app again, so we clear it.
if (persistedAssetTxProofResult == AssetTxProofResult.PENDING) {
persistedAssetTxProofResult = null;
}
trade.setAssetTxProofResult(persistedAssetTxProofResult);
trade.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)
.collect(Collectors.toList()));
@ -2055,7 +2027,6 @@ public abstract class Trade implements Tradable, Model {
",\n errorMessage='" + errorMessage + '\'' +
",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' +
",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' +
",\n assetTxProofResult='" + assetTxProofResult + '\'' +
",\n chatMessages=" + chatMessages +
",\n totalTxFee=" + totalTxFee +
",\n takerFee=" + takerFee +

View file

@ -1,23 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
import haveno.network.http.HttpClient;
public interface AssetTxProofHttpClient extends HttpClient {
}

View file

@ -1,21 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
public interface AssetTxProofModel {
}

View file

@ -1,22 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
public interface AssetTxProofParser<R extends AssetTxProofRequest.Result, T extends AssetTxProofModel> {
R parse(T model, String jsonTxt);
}

View file

@ -1,31 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
import haveno.common.handlers.FaultHandler;
import java.util.function.Consumer;
public interface AssetTxProofRequest<R extends AssetTxProofRequest.Result> {
interface Result {
}
void requestFromService(Consumer<R> resultHandler, FaultHandler faultHandler);
void terminate();
}

View file

@ -1,28 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
import haveno.common.handlers.FaultHandler;
import java.util.function.Consumer;
public interface AssetTxProofRequestsPerTrade {
void requestFromAllServices(Consumer<AssetTxProofResult> resultHandler, FaultHandler faultHandler);
void terminate();
}

View file

@ -1,103 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
import lombok.Getter;
public enum AssetTxProofResult {
UNDEFINED,
FEATURE_DISABLED,
TRADE_LIMIT_EXCEEDED,
INVALID_DATA, // Peer provided invalid data. Might be a scam attempt (e.g. txKey reused)
PAYOUT_TX_ALREADY_PUBLISHED,
DISPUTE_OPENED,
REQUESTS_STARTED(false),
PENDING(false),
// All services completed with a success state
COMPLETED,
// Any service had an error (network, API service)
ERROR,
// Any service failed. Might be that the tx is invalid.
FAILED;
// If isTerminal is set it means that we stop the service
@Getter
private final boolean isTerminal;
@Getter
private String details = "";
@Getter
private int numSuccessResults;
@Getter
private int numRequiredSuccessResults;
@Getter
private int numConfirmations;
@Getter
private int numRequiredConfirmations;
AssetTxProofResult() {
this(true);
}
AssetTxProofResult(boolean isTerminal) {
this.isTerminal = isTerminal;
}
public AssetTxProofResult numSuccessResults(int numSuccessResults) {
this.numSuccessResults = numSuccessResults;
return this;
}
public AssetTxProofResult numRequiredSuccessResults(int numRequiredSuccessResults) {
this.numRequiredSuccessResults = numRequiredSuccessResults;
return this;
}
public AssetTxProofResult numConfirmations(int numConfirmations) {
this.numConfirmations = numConfirmations;
return this;
}
public AssetTxProofResult numRequiredConfirmations(int numRequiredConfirmations) {
this.numRequiredConfirmations = numRequiredConfirmations;
return this;
}
public AssetTxProofResult details(String details) {
this.details = details;
return this;
}
@Override
public String toString() {
return "AssetTxProofResult{" +
"\n details='" + details + '\'' +
",\n isTerminal=" + isTerminal +
",\n numSuccessResults=" + numSuccessResults +
",\n numRequiredSuccessResults=" + numRequiredSuccessResults +
",\n numConfirmations=" + numConfirmations +
",\n numRequiredConfirmations=" + numRequiredConfirmations +
"\n} " + super.toString();
}
}

View file

@ -1,24 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof;
public interface AssetTxProofService {
void onAllServicesInitialized();
void shutDown();
}

View file

@ -1,30 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof.xmr;
import haveno.core.trade.txproof.AssetTxProofHttpClient;
import haveno.network.Socks5ProxyProvider;
import haveno.network.http.HttpClientImpl;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class XmrTxProofHttpClient extends HttpClientImpl implements AssetTxProofHttpClient {
XmrTxProofHttpClient(Socks5ProxyProvider socks5ProxyProvider) {
super(socks5ProxyProvider);
}
}

View file

@ -1,97 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof.xmr;
import com.google.common.annotations.VisibleForTesting;
import haveno.common.app.DevEnv;
import haveno.core.monetary.Volume;
import haveno.core.payment.payload.AssetAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.trade.Trade;
import haveno.core.trade.txproof.AssetTxProofModel;
import haveno.core.user.AutoConfirmSettings;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import static com.google.common.base.Preconditions.checkNotNull;
@SuppressWarnings("SpellCheckingInspection")
@Slf4j
@Value
public class XmrTxProofModel implements AssetTxProofModel {
// Those are values from a valid tx which are set automatically if DevEnv.isDevMode is enabled
public static final String DEV_ADDRESS = "85q13WDADXE26W6h7cStpPMkn8tWpvWgHbpGWWttFEafGXyjsBTXxxyQms4UErouTY5sdKpYHVjQm6SagiCqytseDkzfgub";
public static final String DEV_TX_KEY = "f3ce66c9d395e5e460c8802b2c3c1fff04e508434f9738ee35558aac4678c906";
public static final String DEV_TX_HASH = "5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802";
public static final long DEV_AMOUNT = 8902597360000L;
private final String serviceAddress;
private final AutoConfirmSettings autoConfirmSettings;
private final String tradeId;
private final String txHash;
private final String txKey;
private final String recipientAddress;
private final long amount;
private final Date tradeDate;
XmrTxProofModel(Trade trade, String serviceAddress, AutoConfirmSettings autoConfirmSettings) {
this.serviceAddress = serviceAddress;
this.autoConfirmSettings = autoConfirmSettings;
Volume volume = trade.getVolume();
amount = DevEnv.isDevMode() ?
XmrTxProofModel.DEV_AMOUNT : // For dev testing we need to add the matching address to the dev tx key and dev view key
volume != null ? volume.getValue() * 10000L : 0L; // XMR satoshis have 12 decimal places vs. bitcoin's 8
PaymentAccountPayload sellersPaymentAccountPayload = checkNotNull(trade.getSeller().getPaymentAccountPayload());
recipientAddress = DevEnv.isDevMode() ?
XmrTxProofModel.DEV_ADDRESS : // For dev testing we need to add the matching address to the dev tx key and dev view key
((AssetAccountPayload) sellersPaymentAccountPayload).getAddress();
txHash = trade.getCounterCurrencyTxId();
txKey = trade.getCounterCurrencyExtraData();
tradeDate = trade.getDate();
tradeId = trade.getId();
}
// NumRequiredConfirmations is read just in time. If user changes autoConfirmSettings during requests it will
// be reflected at next result parsing.
int getNumRequiredConfirmations() {
return autoConfirmSettings.getRequiredConfirmations();
}
// Used only for testing
// TODO Use mocking framework in testing to avoid that constructor...
@VisibleForTesting
XmrTxProofModel(String tradeId,
String txHash,
String txKey,
String recipientAddress,
long amount,
Date tradeDate,
AutoConfirmSettings autoConfirmSettings) {
this.tradeId = tradeId;
this.txHash = txHash;
this.txKey = txKey;
this.recipientAddress = recipientAddress;
this.amount = amount;
this.tradeDate = tradeDate;
this.autoConfirmSettings = autoConfirmSettings;
this.serviceAddress = autoConfirmSettings.getServiceAddresses().get(0);
}
}

View file

@ -1,172 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof.xmr;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import haveno.asset.CryptoNoteUtils;
import haveno.common.app.DevEnv;
import haveno.core.trade.txproof.AssetTxProofParser;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
@Slf4j
public class XmrTxProofParser implements AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> {
public static final long MAX_DATE_TOLERANCE = TimeUnit.HOURS.toSeconds(2);
XmrTxProofParser() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("SpellCheckingInspection")
@Override
public XmrTxProofRequest.Result parse(XmrTxProofModel model, String jsonTxt) {
String txHash = model.getTxHash();
try {
JsonObject json = new Gson().fromJson(jsonTxt, JsonObject.class);
if (json == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Empty json"));
}
// there should always be "data" and "status" at the top level
if (json.get("data") == null || !json.get("data").isJsonObject() || json.get("status") == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing data / status fields"));
}
JsonObject jsonData = json.get("data").getAsJsonObject();
String jsonStatus = json.get("status").getAsString();
if (jsonStatus.matches("fail")) {
// The API returns "fail" until the transaction has successfully reached the mempool or if request
// contained invalid data.
// We return TX_NOT_FOUND which will cause a retry later
return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.TX_NOT_FOUND);
} else if (!jsonStatus.matches("success")) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Unhandled status value"));
}
// validate that the address matches
JsonElement jsonAddress = jsonData.get("address");
if (jsonAddress == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing address field"));
} else {
String expectedAddressHex = CryptoNoteUtils.getRawSpendKeyAndViewKey(model.getRecipientAddress());
if (!jsonAddress.getAsString().equalsIgnoreCase(expectedAddressHex)) {
log.warn("Address from json result (convertToRawHex):\n{}\nExpected (convertToRawHex):\n{}\nRecipient address:\n{}",
jsonAddress.getAsString(), expectedAddressHex, model.getRecipientAddress());
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.ADDRESS_INVALID);
}
}
// validate that the txHash matches
JsonElement jsonTxHash = jsonData.get("tx_hash");
if (jsonTxHash == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing tx_hash field"));
} else {
if (!jsonTxHash.getAsString().equalsIgnoreCase(txHash)) {
log.warn("txHash {}, expected: {}", jsonTxHash.getAsString(), txHash);
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.TX_HASH_INVALID);
}
}
// validate that the txKey matches
JsonElement jsonViewkey = jsonData.get("viewkey");
if (jsonViewkey == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing viewkey field"));
} else {
if (!jsonViewkey.getAsString().equalsIgnoreCase(model.getTxKey())) {
log.warn("viewkey {}, expected: {}", jsonViewkey.getAsString(), model.getTxKey());
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.TX_KEY_INVALID);
}
}
// validate that the txDate matches within tolerance
// (except that in dev mode we let this check pass anyway)
JsonElement jsonTimestamp = jsonData.get("tx_timestamp");
if (jsonTimestamp == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing tx_timestamp field"));
} else {
long tradeDateSeconds = model.getTradeDate().getTime() / 1000;
long difference = tradeDateSeconds - jsonTimestamp.getAsLong();
// Accept up to 2 hours difference. Some tolerance is needed if users clock is out of sync
if (difference > MAX_DATE_TOLERANCE && !DevEnv.isDevMode()) {
log.warn("tx_timestamp {}, tradeDate: {}, difference {}",
jsonTimestamp.getAsLong(), tradeDateSeconds, difference);
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING);
}
}
// calculate how many confirms are still needed
int confirmations;
JsonElement jsonConfirmations = jsonData.get("tx_confirmations");
if (jsonConfirmations == null) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error("Missing tx_confirmations field"));
} else {
confirmations = jsonConfirmations.getAsInt();
log.info("Confirmations: {}, xmr txHash: {}", confirmations, txHash);
}
// iterate through the list of outputs, one of them has to match the amount we are trying to verify.
// check that the "match" field is true as well as validating the amount value
// (except that in dev mode we allow any amount as valid)
JsonArray jsonOutputs = jsonData.get("outputs").getAsJsonArray();
boolean anyMatchFound = false;
boolean amountMatches = false;
for (int i = 0; i < jsonOutputs.size(); i++) {
JsonObject out = jsonOutputs.get(i).getAsJsonObject();
if (out.get("match").getAsBoolean()) {
anyMatchFound = true;
long jsonAmount = out.get("amount").getAsLong();
amountMatches = jsonAmount == model.getAmount();
if (amountMatches) {
break;
} else {
log.warn("amount {}, expected: {}", jsonAmount, model.getAmount());
}
}
}
// None of the outputs had a match entry
if (!anyMatchFound) {
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.NO_MATCH_FOUND);
}
// None of the outputs had a match entry
if (!amountMatches) {
return XmrTxProofRequest.Result.FAILED.with(XmrTxProofRequest.Detail.AMOUNT_NOT_MATCHING);
}
int confirmsRequired = model.getNumRequiredConfirmations();
if (confirmations < confirmsRequired) {
return XmrTxProofRequest.Result.PENDING.with(XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS.numConfirmations(confirmations));
} else {
return XmrTxProofRequest.Result.SUCCESS.with(XmrTxProofRequest.Detail.SUCCESS.numConfirmations(confirmations));
}
} catch (JsonParseException | NullPointerException e) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.API_INVALID.error(e.toString()));
} catch (CryptoNoteUtils.CryptoNoteException e) {
return XmrTxProofRequest.Result.ERROR.with(XmrTxProofRequest.Detail.ADDRESS_INVALID.error(e.toString()));
}
}
}

View file

@ -1,289 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof.xmr;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser;
import haveno.common.UserThread;
import haveno.common.app.Version;
import haveno.common.handlers.FaultHandler;
import haveno.common.util.Utilities;
import haveno.core.trade.txproof.AssetTxProofHttpClient;
import haveno.core.trade.txproof.AssetTxProofParser;
import haveno.core.trade.txproof.AssetTxProofRequest;
import haveno.network.Socks5ProxyProvider;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* Requests for the XMR tx proof for a particular trade from a particular service.
* Repeats every 90 sec requests if tx is not confirmed or found yet until MAX_REQUEST_PERIOD of 12 hours is reached.
*/
@Slf4j
@EqualsAndHashCode
class XmrTxProofRequest implements AssetTxProofRequest<XmrTxProofRequest.Result> {
///////////////////////////////////////////////////////////////////////////////////////////
// Enums
///////////////////////////////////////////////////////////////////////////////////////////
enum Result implements AssetTxProofRequest.Result {
PENDING, // Tx not visible in network yet, unconfirmed or not enough confirmations
SUCCESS, // Proof succeeded
FAILED, // Proof failed
ERROR; // Error from service, does not mean that proof failed
@Nullable
@Getter
private Detail detail;
Result with(Detail detail) {
this.detail = detail;
return this;
}
@Override
public String toString() {
return "Result{" +
"\n detail=" + detail +
"\n} " + super.toString();
}
}
enum Detail {
// Pending
TX_NOT_FOUND, // Tx not visible in network yet. Could be also other error
PENDING_CONFIRMATIONS,
SUCCESS,
// Error states
CONNECTION_FAILURE,
API_INVALID,
// Failure states
TX_HASH_INVALID,
TX_KEY_INVALID,
ADDRESS_INVALID,
NO_MATCH_FOUND,
AMOUNT_NOT_MATCHING,
TRADE_DATE_NOT_MATCHING,
NO_RESULTS_TIMEOUT;
@Getter
private int numConfirmations;
@Nullable
@Getter
private String errorMsg;
public Detail error(String errorMsg) {
this.errorMsg = errorMsg;
return this;
}
public Detail numConfirmations(int numConfirmations) {
this.numConfirmations = numConfirmations;
return this;
}
@Override
public String toString() {
return "Detail{" +
"\n numConfirmations=" + numConfirmations +
",\n errorMsg='" + errorMsg + '\'' +
"\n} " + super.toString();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Static fields
///////////////////////////////////////////////////////////////////////////////////////////
private static final long REPEAT_REQUEST_PERIOD = TimeUnit.SECONDS.toMillis(90);
private static final long MAX_REQUEST_PERIOD = TimeUnit.HOURS.toMillis(12);
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
"XmrTransferProofRequester", 3, 5, 10 * 60);
private final AssetTxProofParser<XmrTxProofRequest.Result, XmrTxProofModel> parser;
private final XmrTxProofModel model;
private final AssetTxProofHttpClient httpClient;
private final long firstRequest;
private boolean terminated;
@Getter
@Nullable
private Result result;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
XmrTxProofRequest(Socks5ProxyProvider socks5ProxyProvider,
XmrTxProofModel model) {
this.parser = new XmrTxProofParser();
this.model = model;
httpClient = new XmrTxProofHttpClient(socks5ProxyProvider);
// localhost, LAN address, or *.local FQDN starts with http://, don't use Tor
if (model.getServiceAddress().regionMatches(0, "http:", 0, 5)) {
httpClient.setBaseUrl(model.getServiceAddress());
httpClient.setIgnoreSocks5Proxy(true);
// any non-onion FQDN starts with https://, use Tor
} else if (model.getServiceAddress().regionMatches(0, "https:", 0, 6)) {
httpClient.setBaseUrl(model.getServiceAddress());
httpClient.setIgnoreSocks5Proxy(false);
// it's a raw onion so add http:// and use Tor proxy
} else {
httpClient.setBaseUrl("http://" + model.getServiceAddress());
httpClient.setIgnoreSocks5Proxy(false);
}
terminated = false;
firstRequest = System.currentTimeMillis();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("SpellCheckingInspection")
@Override
public void requestFromService(Consumer<Result> resultHandler, FaultHandler faultHandler) {
if (terminated) {
// the XmrTransferProofService has asked us to terminate i.e. not make any further api calls
// this scenario may happen if a re-request is scheduled from the callback below
log.warn("Not starting {} as we have already terminated.", this);
return;
}
if (httpClient.hasPendingRequest()) {
log.warn("We have a pending request open. We ignore that request. httpClient {}", httpClient);
return;
}
// Timeout handing is delegated to the connection timeout handling in httpClient.
ListenableFuture<Result> future = executorService.submit(() -> {
Thread.currentThread().setName("XmrTransferProofRequest-" + this.getShortId());
String param = "/api/outputs?txhash=" + model.getTxHash() +
"&address=" + model.getRecipientAddress() +
"&viewkey=" + model.getTxKey() +
"&txprove=1";
log.info("Param {} for {}", param, this);
String json = httpClient.get(param, "User-Agent", "haveno/" + Version.VERSION);
try {
String prettyJson = new GsonBuilder().setPrettyPrinting().create().toJson(new JsonParser().parse(json));
log.info("Response json from {}\n{}", this, prettyJson);
} catch (Throwable error) {
log.error("Pretty print caused a {}: raw json={}", error, json);
}
Result result = parser.parse(model, json);
log.info("Result from {}\n{}", this, result);
return result;
});
Futures.addCallback(future, new FutureCallback<>() {
public void onSuccess(Result result) {
XmrTxProofRequest.this.result = result;
if (terminated) {
log.warn("We received {} but {} was terminated already. We do not process result.", result, this);
return;
}
switch (result) {
case PENDING:
if (isTimeOutReached()) {
log.warn("{} took too long without a success or failure/error result We give up. " +
"Might be that the transaction was never published.", this);
// If we reached out timeout we return with an error.
UserThread.execute(() -> resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.NO_RESULTS_TIMEOUT)));
} else {
UserThread.runAfter(() -> requestFromService(resultHandler, faultHandler), REPEAT_REQUEST_PERIOD, TimeUnit.MILLISECONDS);
// We update our listeners
UserThread.execute(() -> resultHandler.accept(result));
}
break;
case SUCCESS:
log.info("{} succeeded", result);
UserThread.execute(() -> resultHandler.accept(result));
terminate();
break;
case FAILED:
case ERROR:
UserThread.execute(() -> resultHandler.accept(result));
terminate();
break;
default:
log.warn("Unexpected result {}", result);
break;
}
}
public void onFailure(@NotNull Throwable throwable) {
String errorMessage = this + " failed with error " + throwable.toString();
faultHandler.handleFault(errorMessage, throwable);
UserThread.execute(() ->
resultHandler.accept(XmrTxProofRequest.Result.ERROR.with(Detail.CONNECTION_FAILURE.error(errorMessage))));
}
}, MoreExecutors.directExecutor());
}
@Override
public void terminate() {
terminated = true;
}
// Convenient for logging
@Override
public String toString() {
return "Request at: " + model.getServiceAddress() + " for trade: " + model.getTradeId();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private String getShortId() {
return Utilities.getShortId(model.getTradeId()) + " @ " + model.getServiceAddress().substring(0, 6);
}
private boolean isTimeOutReached() {
return System.currentTimeMillis() - firstRequest > MAX_REQUEST_PERIOD;
}
}

View file

@ -1,338 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof.xmr;
import haveno.common.handlers.FaultHandler;
import haveno.core.filter.FilterManager;
import haveno.core.locale.Res;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.mediation.MediationManager;
import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.txproof.AssetTxProofRequestsPerTrade;
import haveno.core.trade.txproof.AssetTxProofResult;
import haveno.core.user.AutoConfirmSettings;
import haveno.network.Socks5ProxyProvider;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/**
* Handles the XMR tx proof requests for multiple services per trade.
*/
@Slf4j
class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade {
@Getter
private final Trade trade;
private final AutoConfirmSettings autoConfirmSettings;
private final MediationManager mediationManager;
private final FilterManager filterManager;
private final RefundManager refundManager;
private final Socks5ProxyProvider socks5ProxyProvider;
private int numRequiredSuccessResults;
private final Set<XmrTxProofRequest> requests = new HashSet<>();
private int numSuccessResults;
private ChangeListener<Trade.State> tradeStateListener;
private AutoConfirmSettings.Listener autoConfirmSettingsListener;
private ListChangeListener<Dispute> mediationListener, refundListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
XmrTxProofRequestsPerTrade(Socks5ProxyProvider socks5ProxyProvider,
Trade trade,
AutoConfirmSettings autoConfirmSettings,
MediationManager mediationManager,
FilterManager filterManager,
RefundManager refundManager) {
this.socks5ProxyProvider = socks5ProxyProvider;
this.trade = trade;
this.autoConfirmSettings = autoConfirmSettings;
this.mediationManager = mediationManager;
this.filterManager = filterManager;
this.refundManager = refundManager;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void requestFromAllServices(Consumer<AssetTxProofResult> resultHandler, FaultHandler faultHandler) {
// isTradeAmountAboveLimit
if (isTradeAmountAboveLimit(trade)) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.TRADE_LIMIT_EXCEEDED);
return;
}
// isPayoutPublished
if (trade.isPayoutPublished()) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED);
return;
}
// IsEnabled()
// We will stop all our services if the user changes the enable state in the AutoConfirmSettings
if (!autoConfirmSettings.isEnabled()) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED);
return;
}
addSettingsListener(resultHandler);
// TradeState
setupTradeStateListener(resultHandler);
// We checked initially for current trade state so no need to check again here
// Check if mediation dispute and add listener
ObservableList<Dispute> mediationDisputes = mediationManager.getDisputesAsObservableList();
if (isDisputed(mediationDisputes)) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED);
return;
}
setupMediationListener(resultHandler, mediationDisputes);
// Check if arbitration dispute and add listener
ObservableList<Dispute> refundDisputes = refundManager.getDisputesAsObservableList();
if (isDisputed(refundDisputes)) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED);
return;
}
setupArbitrationListener(resultHandler, refundDisputes);
// All good so we start
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.REQUESTS_STARTED);
// We set serviceAddresses at request time. If user changes AutoConfirmSettings after request has started
// it will have no impact on serviceAddresses and numRequiredSuccessResults.
// Thought numRequiredConfirmations can be changed during request process and will be read from
// autoConfirmSettings at result parsing.
List<String> serviceAddresses = autoConfirmSettings.getServiceAddresses();
numRequiredSuccessResults = serviceAddresses.size();
for (String serviceAddress : serviceAddresses) {
if (filterManager.isAutoConfExplorerBanned(serviceAddress)) {
log.warn("Filtered out auto-confirmation address: {}", serviceAddress);
continue; // #4683: filter for auto-confirm explorers
}
XmrTxProofModel model = new XmrTxProofModel(trade, serviceAddress, autoConfirmSettings);
XmrTxProofRequest request = new XmrTxProofRequest(socks5ProxyProvider, model);
log.info("{} created", request);
requests.add(request);
request.requestFromService(result -> {
// If we ever received an error or failed result we terminate and do not process any
// future result anymore to avoid that we overwrite out state with success.
if (wasTerminated()) {
return;
}
AssetTxProofResult assetTxProofResult;
if (trade.isPayoutPublished()) {
assetTxProofResult = AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED;
callResultHandlerAndMaybeTerminate(resultHandler, assetTxProofResult);
return;
}
switch (result) {
case PENDING:
// We expect repeated PENDING results with different details
assetTxProofResult = getAssetTxProofResultForPending(result);
break;
case SUCCESS:
numSuccessResults++;
if (numSuccessResults < numRequiredSuccessResults) {
// Request is success but not all have completed yet.
int remaining = numRequiredSuccessResults - numSuccessResults;
log.info("{} succeeded. We have {} remaining request(s) open.",
request, remaining);
assetTxProofResult = getAssetTxProofResultForPending(result);
} else {
// All our services have returned a SUCCESS result so we
// have completed on the service level.
log.info("All {} tx proof requests for trade {} have been successful.",
numRequiredSuccessResults, trade.getShortId());
XmrTxProofRequest.Detail detail = result.getDetail();
assetTxProofResult = AssetTxProofResult.COMPLETED
.numSuccessResults(numSuccessResults)
.numRequiredSuccessResults(numRequiredSuccessResults)
.numConfirmations(detail != null ? detail.getNumConfirmations() : 0)
.numRequiredConfirmations(autoConfirmSettings.getRequiredConfirmations());
}
break;
case FAILED:
log.warn("{} failed. " +
"This might not mean that the XMR transfer was invalid but you have to check yourself " +
"if the XMR transfer was correct. {}",
request, result);
assetTxProofResult = AssetTxProofResult.FAILED;
break;
case ERROR:
default:
log.warn("{} resulted in an error. " +
"This might not mean that the XMR transfer was invalid but can be a network or " +
"service problem. {}",
request, result);
assetTxProofResult = AssetTxProofResult.ERROR;
break;
}
callResultHandlerAndMaybeTerminate(resultHandler, assetTxProofResult);
},
faultHandler);
}
}
private boolean wasTerminated() {
return requests.isEmpty();
}
private void addSettingsListener(Consumer<AssetTxProofResult> resultHandler) {
autoConfirmSettingsListener = () -> {
if (!autoConfirmSettings.isEnabled()) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.FEATURE_DISABLED);
}
};
autoConfirmSettings.addListener(autoConfirmSettingsListener);
}
private void setupTradeStateListener(Consumer<AssetTxProofResult> resultHandler) {
tradeStateListener = (observable, oldValue, newValue) -> {
if (trade.isPayoutPublished()) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.PAYOUT_TX_ALREADY_PUBLISHED);
}
};
trade.stateProperty().addListener(tradeStateListener);
}
private void setupArbitrationListener(Consumer<AssetTxProofResult> resultHandler,
ObservableList<Dispute> refundDisputes) {
refundListener = c -> {
c.next();
if (c.wasAdded() && isDisputed(c.getAddedSubList())) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED);
}
};
refundDisputes.addListener(refundListener);
}
private void setupMediationListener(Consumer<AssetTxProofResult> resultHandler,
ObservableList<Dispute> mediationDisputes) {
mediationListener = c -> {
c.next();
if (c.wasAdded() && isDisputed(c.getAddedSubList())) {
callResultHandlerAndMaybeTerminate(resultHandler, AssetTxProofResult.DISPUTE_OPENED);
}
};
mediationDisputes.addListener(mediationListener);
}
@Override
public void terminate() {
requests.forEach(XmrTxProofRequest::terminate);
requests.clear();
if (tradeStateListener != null) {
trade.stateProperty().removeListener(tradeStateListener);
}
if (autoConfirmSettingsListener != null) {
autoConfirmSettings.removeListener(autoConfirmSettingsListener);
}
if (mediationListener != null) {
mediationManager.getDisputesAsObservableList().removeListener(mediationListener);
}
if (refundListener != null) {
refundManager.getDisputesAsObservableList().removeListener(refundListener);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void callResultHandlerAndMaybeTerminate(Consumer<AssetTxProofResult> resultHandler,
AssetTxProofResult assetTxProofResult) {
resultHandler.accept(assetTxProofResult);
if (assetTxProofResult.isTerminal()) {
terminate();
}
}
private AssetTxProofResult getAssetTxProofResultForPending(XmrTxProofRequest.Result result) {
XmrTxProofRequest.Detail detail = result.getDetail();
int numConfirmations = detail != null ? detail.getNumConfirmations() : 0;
log.info("{} returned with numConfirmations {}",
result, numConfirmations);
String detailString = "";
if (XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS == detail) {
detailString = Res.get("portfolio.pending.autoConf.state.confirmations",
numConfirmations, autoConfirmSettings.getRequiredConfirmations());
} else if (XmrTxProofRequest.Detail.TX_NOT_FOUND == detail) {
detailString = Res.get("portfolio.pending.autoConf.state.txNotFound");
}
return AssetTxProofResult.PENDING
.numSuccessResults(numSuccessResults)
.numRequiredSuccessResults(numRequiredSuccessResults)
.numConfirmations(detail != null ? detail.getNumConfirmations() : 0)
.numRequiredConfirmations(autoConfirmSettings.getRequiredConfirmations())
.details(detailString);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Validation
///////////////////////////////////////////////////////////////////////////////////////////
private boolean isTradeAmountAboveLimit(Trade trade) {
BigInteger tradeAmount = trade.getAmount();
BigInteger tradeLimit = BigInteger.valueOf(autoConfirmSettings.getTradeLimit());
if (tradeAmount != null && tradeAmount.compareTo(tradeLimit) > 0) {
log.warn("Trade amount {} is higher than limit from auto-conf setting {}.",
HavenoUtils.formatXmr(tradeAmount, true), HavenoUtils.formatXmr(tradeLimit, true));
return true;
}
return false;
}
private boolean isDisputed(List<? extends Dispute> disputes) {
return disputes.stream().anyMatch(e -> e.getTradeId().equals(trade.getId()));
}
}

View file

@ -1,390 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.txproof.xmr;
import haveno.common.app.DevEnv;
import haveno.core.api.CoreMoneroConnectionsService;
import haveno.core.filter.FilterManager;
import haveno.core.locale.Res;
import haveno.core.support.dispute.mediation.MediationManager;
import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.SellerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.failed.FailedTradesManager;
import haveno.core.trade.protocol.SellerProtocol;
import haveno.core.trade.txproof.AssetTxProofResult;
import haveno.core.trade.txproof.AssetTxProofService;
import haveno.core.user.AutoConfirmSettings;
import haveno.core.user.Preferences;
import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.P2PService;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicBinding;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Entry point for clients to request tx proof and trigger auto-confirm if all conditions
* are met.
*/
@Slf4j
@Singleton
public class XmrTxProofService implements AssetTxProofService {
private final FilterManager filterManager;
private final Preferences preferences;
private final TradeManager tradeManager;
private final ClosedTradableManager closedTradableManager;
private final FailedTradesManager failedTradesManager;
private final MediationManager mediationManager;
private final RefundManager refundManager;
private final P2PService p2PService;
private final CoreMoneroConnectionsService connectionService;
private final Socks5ProxyProvider socks5ProxyProvider;
private final Map<String, XmrTxProofRequestsPerTrade> servicesByTradeId = new HashMap<>();
private AutoConfirmSettings autoConfirmSettings;
private final Map<String, ChangeListener<Trade.State>> tradeStateListenerMap = new HashMap<>();
private ChangeListener<Number> xmrPeersListener, xmrBlockListener;
private BootstrapListener bootstrapListener;
private MonadicBinding<Boolean> p2pNetworkAndWalletReady;
private ChangeListener<Boolean> p2pNetworkAndWalletReadyListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess")
@Inject
public XmrTxProofService(FilterManager filterManager,
Preferences preferences,
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
FailedTradesManager failedTradesManager,
MediationManager mediationManager,
RefundManager refundManager,
P2PService p2PService,
CoreMoneroConnectionsService connectionService,
Socks5ProxyProvider socks5ProxyProvider) {
this.filterManager = filterManager;
this.preferences = preferences;
this.tradeManager = tradeManager;
this.closedTradableManager = closedTradableManager;
this.failedTradesManager = failedTradesManager;
this.mediationManager = mediationManager;
this.refundManager = refundManager;
this.p2PService = p2PService;
this.connectionService = connectionService;
this.socks5ProxyProvider = socks5ProxyProvider;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAllServicesInitialized() {
// As we might trigger the payout tx we want to be sure that we are well connected to the Bitcoin network.
// onAllServicesInitialized is called once we have received the initial data but we want to have our
// hidden service published and upDatedDataResponse received before we start.
BooleanProperty isP2pBootstrapped = isP2pBootstrapped();
BooleanProperty hasSufficientXmrPeers = hasSufficientXmrPeers();
BooleanProperty isXmrBlockDownloadComplete = isXmrBlockDownloadComplete();
if (isP2pBootstrapped.get() && hasSufficientXmrPeers.get() && isXmrBlockDownloadComplete.get()) {
onP2pNetworkAndWalletReady();
} else {
p2pNetworkAndWalletReady = EasyBind.combine(isP2pBootstrapped, hasSufficientXmrPeers, isXmrBlockDownloadComplete,
(bootstrapped, sufficientPeers, downloadComplete) ->
bootstrapped && sufficientPeers && downloadComplete);
p2pNetworkAndWalletReadyListener = (observable, oldValue, newValue) -> {
if (newValue) {
onP2pNetworkAndWalletReady();
}
};
p2pNetworkAndWalletReady.subscribe(p2pNetworkAndWalletReadyListener);
}
}
@Override
public void shutDown() {
servicesByTradeId.values().forEach(XmrTxProofRequestsPerTrade::terminate);
servicesByTradeId.clear();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void onP2pNetworkAndWalletReady() {
if (p2pNetworkAndWalletReady != null) {
p2pNetworkAndWalletReady.removeListener(p2pNetworkAndWalletReadyListener);
p2pNetworkAndWalletReady = null;
p2pNetworkAndWalletReadyListener = null;
}
if (!preferences.findAutoConfirmSettings("XMR").isPresent()) {
log.error("AutoConfirmSettings is not present");
return;
}
autoConfirmSettings = preferences.findAutoConfirmSettings("XMR").get();
// We register a listener to stop running services. For new trades we check anyway in the trade validation
filterManager.filterProperty().addListener((observable, oldValue, newValue) -> {
if (isAutoConfDisabledByFilter()) {
servicesByTradeId.values().stream().map(XmrTxProofRequestsPerTrade::getTrade).forEach(trade ->
trade.setAssetTxProofResult(AssetTxProofResult.FEATURE_DISABLED
.details(Res.get("portfolio.pending.autoConf.state.filterDisabledFeature"))));
tradeManager.requestPersistence();
shutDown();
}
});
// We listen on new trades
ObservableList<Trade> tradableList = tradeManager.getObservableList();
tradableList.addListener((ListChangeListener<Trade>) c -> {
c.next();
if (c.wasAdded()) {
processTrades(c.getAddedSubList());
}
});
// Process existing trades
processTrades(tradableList);
}
private void processTrades(List<? extends Trade> trades) {
trades.stream()
.filter(trade -> trade instanceof SellerTrade)
.map(trade -> (SellerTrade) trade)
.filter(this::isXmrTrade)
.filter(trade -> !trade.isPaymentReceived()) // Phase name is from the time when it was fiat only. Means counter currency (XMR) received.
.forEach(this::processTradeOrAddListener);
}
// Basic requirements are fulfilled.
// We process further if we are in the expected state or register a listener
private void processTradeOrAddListener(SellerTrade trade) {
if (isExpectedTradeState(trade.getState())) {
startRequestsIfValid(trade);
} else {
// We are expecting SELLER_RECEIVED_PAYMENT_SENT_MSG in the future, so listen on changes
ChangeListener<Trade.State> tradeStateListener = (observable, oldValue, newValue) -> {
if (isExpectedTradeState(newValue)) {
ChangeListener<Trade.State> listener = tradeStateListenerMap.remove(trade.getId());
if (listener != null) {
trade.stateProperty().removeListener(listener);
}
startRequestsIfValid(trade);
}
};
tradeStateListenerMap.put(trade.getId(), tradeStateListener);
trade.stateProperty().addListener(tradeStateListener);
}
}
private void startRequestsIfValid(SellerTrade trade) {
String txId = trade.getCounterCurrencyTxId();
String txHash = trade.getCounterCurrencyExtraData();
if (is32BitHexStringInValid(txId) || is32BitHexStringInValid(txHash)) {
trade.setAssetTxProofResult(AssetTxProofResult.INVALID_DATA.details(Res.get("portfolio.pending.autoConf.state.txKeyOrTxIdInvalid")));
tradeManager.requestPersistence();
return;
}
if (isAutoConfDisabledByFilter()) {
trade.setAssetTxProofResult(AssetTxProofResult.FEATURE_DISABLED
.details(Res.get("portfolio.pending.autoConf.state.filterDisabledFeature")));
tradeManager.requestPersistence();
return;
}
if (wasTxKeyReUsed(trade, tradeManager.getObservableList())) {
trade.setAssetTxProofResult(AssetTxProofResult.INVALID_DATA
.details(Res.get("portfolio.pending.autoConf.state.xmr.txKeyReused")));
tradeManager.requestPersistence();
return;
}
startRequests(trade);
}
private void startRequests(SellerTrade trade) {
XmrTxProofRequestsPerTrade service = new XmrTxProofRequestsPerTrade(socks5ProxyProvider,
trade,
autoConfirmSettings,
mediationManager,
filterManager,
refundManager);
servicesByTradeId.put(trade.getId(), service);
service.requestFromAllServices(
assetTxProofResult -> {
trade.setAssetTxProofResult(assetTxProofResult);
if (assetTxProofResult == AssetTxProofResult.COMPLETED) {
log.info("###########################################################################################");
log.info("We auto-confirm trade {} as our all our services for the tx proof completed successfully", trade.getShortId());
log.info("###########################################################################################");
((SellerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentReceived(() -> {
}, errorMessage -> {
});
}
if (assetTxProofResult.isTerminal()) {
servicesByTradeId.remove(trade.getId());
}
tradeManager.requestPersistence();
},
(errorMessage, throwable) -> {
log.error(errorMessage);
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Startup checks
///////////////////////////////////////////////////////////////////////////////////////////
private BooleanProperty isXmrBlockDownloadComplete() {
BooleanProperty result = new SimpleBooleanProperty();
if (connectionService.isDownloadComplete()) {
result.set(true);
} else {
xmrBlockListener = (observable, oldValue, newValue) -> {
if (connectionService.isDownloadComplete()) {
connectionService.downloadPercentageProperty().removeListener(xmrBlockListener);
result.set(true);
}
};
connectionService.downloadPercentageProperty().addListener(xmrBlockListener);
}
return result;
}
private BooleanProperty hasSufficientXmrPeers() {
BooleanProperty result = new SimpleBooleanProperty();
if (connectionService.hasSufficientPeersForBroadcast()) {
result.set(true);
} else {
xmrPeersListener = (observable, oldValue, newValue) -> {
if (connectionService.hasSufficientPeersForBroadcast()) {
connectionService.numPeersProperty().removeListener(xmrPeersListener);
result.set(true);
}
};
connectionService.numPeersProperty().addListener(xmrPeersListener);
}
return result;
}
private BooleanProperty isP2pBootstrapped() {
BooleanProperty result = new SimpleBooleanProperty();
if (p2PService.isBootstrapped()) {
result.set(true);
} else {
bootstrapListener = new BootstrapListener() {
@Override
public void onUpdatedDataReceived() {
p2PService.removeP2PServiceListener(bootstrapListener);
result.set(true);
}
};
p2PService.addP2PServiceListener(bootstrapListener);
}
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Validation
///////////////////////////////////////////////////////////////////////////////////////////
private boolean isXmrTrade(Trade trade) {
return (checkNotNull(trade.getOffer()).getCurrencyCode().equals("XMR"));
}
private boolean isExpectedTradeState(Trade.State newValue) {
return newValue == Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG;
}
private boolean is32BitHexStringInValid(String hexString) {
if (hexString == null || hexString.isEmpty() || !hexString.matches("[a-fA-F0-9]{64}")) {
log.warn("Invalid hexString: {}", hexString);
return true;
}
return false;
}
private boolean isAutoConfDisabledByFilter() {
return filterManager.getFilter() != null &&
filterManager.getFilter().isDisableAutoConf();
}
private boolean wasTxKeyReUsed(Trade trade, List<Trade> activeTrades) {
// For dev testing we reuse test data so we ignore that check
if (DevEnv.isDevMode()) {
return false;
}
// We need to prevent that a user tries to scam by reusing a txKey and txHash of a previous XMR trade with
// the same user (same address) and same amount. We check only for the txKey as a same txHash but different
// txKey is not possible to get a valid result at proof.
Stream<Trade> failedAndOpenTrades = Stream.concat(activeTrades.stream(), failedTradesManager.getObservableList().stream());
Stream<Trade> closedTrades = closedTradableManager.getObservableList().stream()
.filter(tradable -> tradable instanceof Trade)
.map(tradable -> (Trade) tradable);
Stream<Trade> allTrades = Stream.concat(failedAndOpenTrades, closedTrades);
String txKey = trade.getCounterCurrencyExtraData();
return allTrades
.filter(t -> !t.getId().equals(trade.getId())) // ignore same trade
.anyMatch(t -> {
String extra = t.getCounterCurrencyExtraData();
if (extra == null) {
return false;
}
boolean alreadyUsed = extra.equals(txKey);
if (alreadyUsed) {
log.warn("Peer used the XMR tx key already at another trade with trade ID {}. " +
"This might be a scam attempt.", t.getId());
}
return alreadyUsed;
});
}
}

View file

@ -18,10 +18,6 @@
package haveno.core.xmr.wallet;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.util.Tuple2;
import haveno.core.user.Preferences;
import haveno.core.xmr.exceptions.AddressEntryException;
@ -31,7 +27,6 @@ import haveno.core.xmr.exceptions.WalletException;
import haveno.core.xmr.model.AddressEntry;
import haveno.core.xmr.model.AddressEntryList;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
@ -39,16 +34,13 @@ import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.SegwitAddress;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptPattern;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.bouncycastle.crypto.params.KeyParameter;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
@ -403,162 +395,6 @@ public class BtcWalletService extends WalletService {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Double spend unconfirmed transaction (unlock in case we got into a tx with a too low mining fee)
///////////////////////////////////////////////////////////////////////////////////////////
public void doubleSpendTransaction(String txId, Runnable resultHandler, ErrorMessageHandler errorMessageHandler)
throws InsufficientFundsException {
AddressEntry addressEntry = getFreshAddressEntry();
checkNotNull(addressEntry.getAddress(), "addressEntry.getAddress() must not be null");
Optional<Transaction> transactionOptional = wallet.getTransactions(true).stream()
.filter(t -> t.getTxId().toString().equals(txId))
.findAny();
if (transactionOptional.isPresent()) {
Transaction txToDoubleSpend = transactionOptional.get();
Address toAddress = addressEntry.getAddress();
final TransactionConfidence.ConfidenceType confidenceType = txToDoubleSpend.getConfidence().getConfidenceType();
if (confidenceType == TransactionConfidence.ConfidenceType.PENDING) {
log.debug("txToDoubleSpend no. of inputs " + txToDoubleSpend.getInputs().size());
Transaction newTransaction = new Transaction(params);
txToDoubleSpend.getInputs().stream().forEach(input -> {
final TransactionOutput connectedOutput = input.getConnectedOutput();
if (connectedOutput != null &&
connectedOutput.isMine(wallet) &&
connectedOutput.getParentTransaction() != null &&
connectedOutput.getParentTransaction().getConfidence() != null &&
input.getValue() != null) {
//if (connectedOutput.getParentTransaction().getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) {
newTransaction.addInput(new TransactionInput(params,
newTransaction,
new byte[]{},
new TransactionOutPoint(params, input.getOutpoint().getIndex(),
new Transaction(params, connectedOutput.getParentTransaction().bitcoinSerialize())),
Coin.valueOf(input.getValue().value)));
/* } else {
log.warn("Confidence of parent tx is not of type BUILDING: ConfidenceType=" +
connectedOutput.getParentTransaction().getConfidence().getConfidenceType());
}*/
}
}
);
log.info("newTransaction no. of inputs " + newTransaction.getInputs().size());
log.info("newTransaction vsize in vkB " + newTransaction.getVsize() / 1024);
if (!newTransaction.getInputs().isEmpty()) {
Coin amount = Coin.valueOf(newTransaction.getInputs().stream()
.mapToLong(input -> input.getValue() != null ? input.getValue().value : 0)
.sum());
newTransaction.addOutput(amount, toAddress);
try {
Coin fee;
int counter = 0;
int txVsize = 0;
Transaction tx;
SendRequest sendRequest;
Coin txFeeForWithdrawalPerVbyte = getTxFeeForWithdrawalPerVbyte();
do {
counter++;
fee = txFeeForWithdrawalPerVbyte.multiply(txVsize);
newTransaction.clearOutputs();
newTransaction.addOutput(amount.subtract(fee), toAddress);
sendRequest = SendRequest.forTx(newTransaction);
sendRequest.fee = fee;
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
sendRequest.aesKey = aesKey;
sendRequest.coinSelector = new BtcCoinSelector(toAddress, preferences.getIgnoreDustThreshold());
sendRequest.changeAddress = toAddress;
wallet.completeTx(sendRequest);
tx = sendRequest.tx;
txVsize = tx.getVsize();
printTx("FeeEstimationTransaction", tx);
sendRequest.tx.getOutputs().forEach(o -> log.debug("Output value " + o.getValue().toFriendlyString()));
}
while (feeEstimationNotSatisfied(counter, tx));
if (counter == 10)
log.error("Could not calculate the fee. Tx=" + tx);
Wallet.SendResult sendResult = null;
try {
sendRequest = SendRequest.forTx(newTransaction);
sendRequest.fee = fee;
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
sendRequest.aesKey = aesKey;
sendRequest.coinSelector = new BtcCoinSelector(toAddress, preferences.getIgnoreDustThreshold());
sendRequest.changeAddress = toAddress;
sendResult = wallet.sendCoins(sendRequest);
} catch (InsufficientMoneyException e) {
// in some cases getFee did not calculate correctly and we still get an InsufficientMoneyException
log.warn("We still have a missing fee " + (e.missing != null ? e.missing.toFriendlyString() : ""));
amount = amount.subtract(e.missing);
newTransaction.clearOutputs();
newTransaction.addOutput(amount, toAddress);
sendRequest = SendRequest.forTx(newTransaction);
sendRequest.fee = fee;
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
sendRequest.aesKey = aesKey;
sendRequest.coinSelector = new BtcCoinSelector(toAddress,
preferences.getIgnoreDustThreshold(), false);
sendRequest.changeAddress = toAddress;
try {
sendResult = wallet.sendCoins(sendRequest);
printTx("FeeEstimationTransaction", newTransaction);
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
} catch (InsufficientMoneyException e2) {
errorMessageHandler.handleErrorMessage("We did not get the correct fee calculated. " + (e2.missing != null ? e2.missing.toFriendlyString() : ""));
}
}
if (sendResult != null) {
log.info("Broadcasting double spending transaction. " + sendResult.tx);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(Transaction result) {
log.info("Double spending transaction published. " + result);
resultHandler.run();
}
@Override
public void onFailure(@NotNull Throwable t) {
log.error("Broadcasting double spending transaction failed. " + t.getMessage());
errorMessageHandler.handleErrorMessage(t.getMessage());
}
}, MoreExecutors.directExecutor());
}
} catch (InsufficientMoneyException e) {
throw new InsufficientFundsException("The fees for that transaction exceed the available funds " +
"or the resulting output value is below the min. dust value:\n" +
"Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null"));
}
} else {
String errorMessage = "We could not find inputs we control in the transaction we want to double spend.";
log.warn(errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}
} else if (confidenceType == TransactionConfidence.ConfidenceType.BUILDING) {
errorMessageHandler.handleErrorMessage("That transaction is already in the blockchain so we cannot double spend it.");
} else if (confidenceType == TransactionConfidence.ConfidenceType.DEAD) {
errorMessageHandler.handleErrorMessage("One of the inputs of that transaction has been already double spent.");
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Withdrawal Fee calculation
///////////////////////////////////////////////////////////////////////////////////////////
@ -701,54 +537,6 @@ public class BtcWalletService extends WalletService {
// Withdrawal Send
///////////////////////////////////////////////////////////////////////////////////////////
public String sendFunds(String fromAddress,
String toAddress,
Coin receiverAmount,
Coin fee,
@Nullable KeyParameter aesKey,
@SuppressWarnings("SameParameterValue") AddressEntry.Context context,
@Nullable String memo,
FutureCallback<Transaction> callback) throws AddressFormatException,
AddressEntryException, InsufficientMoneyException {
SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context);
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
if (memo != null) {
sendResult.tx.setMemo(memo);
}
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
return sendResult.tx.getTxId().toString();
}
public Transaction sendFundsForMultipleAddresses(Set<String> fromAddresses,
String toAddress,
Coin receiverAmount,
Coin fee,
@Nullable String changeAddress,
@Nullable KeyParameter aesKey,
@Nullable String memo,
FutureCallback<Transaction> callback) throws AddressFormatException,
AddressEntryException, InsufficientMoneyException {
SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey);
Wallet.SendResult sendResult = wallet.sendCoins(request);
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
if (memo != null) {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
return sendResult.tx;
}
private SendRequest getSendRequest(String fromAddress,
String toAddress,
Coin amount,

View file

@ -994,30 +994,6 @@ public class TradeWalletService {
return new Tuple2<>(txId, signedTxHex);
}
public void emergencyPublishPayoutTxFrom2of2MultiSig(String signedTxHex, TxBroadcaster.Callback callback)
throws AddressFormatException, TransactionVerificationException, WalletException {
Transaction payoutTx = new Transaction(params, Utils.HEX.decode(signedTxHex));
WalletService.printTx("payoutTx", payoutTx);
WalletService.verifyTransaction(payoutTx);
WalletService.checkWalletConsistency(wallet);
broadcastTx(payoutTx, callback, 20);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Broadcast tx
///////////////////////////////////////////////////////////////////////////////////////////
public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback) {
checkNotNull(walletConfig);
TxBroadcaster.broadcastTx(wallet, walletConfig.peerGroup(), tx, callback);
}
public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback, int timeoutInSec) {
checkNotNull(walletConfig);
TxBroadcaster.broadcastTx(wallet, walletConfig.peerGroup(), tx, callback, timeoutInSec);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Misc

View file

@ -1,146 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.xmr.wallet;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.core.xmr.exceptions.TxBroadcastException;
import haveno.core.xmr.exceptions.TxBroadcastTimeoutException;
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Utils;
import org.bitcoinj.wallet.Wallet;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class TxBroadcaster {
public interface Callback {
void onSuccess(Transaction transaction);
default void onTimeout(TxBroadcastTimeoutException exception) {
Transaction tx = exception.getLocalTx();
if (tx != null) {
// We optimistically assume that the tx broadcast succeeds later and call onSuccess on the callback handler.
// This behaviour carries less potential problems than if we would trigger a failure (e.g. which would cause
// a failed create offer attempt or failed take offer attempt).
// We have no guarantee how long it will take to get the information that sufficiently many BTC nodes have
// reported back to BitcoinJ that the tx is in their mempool.
// In normal situations that's very fast but in some cases it can take minutes (mostly related to Tor
// connection issues). So if we just go on in the application logic and treat it as successful and the
// tx will be broadcast successfully later all is fine.
// If it will fail to get broadcast, it will lead to a failure state, the same as if we would trigger a
// failure due the timeout.
// So we can assume that this behaviour will lead to less problems as otherwise.
// Long term we should implement better monitoring for Tor and the provided Bitcoin nodes to find out
// why those delays happen and add some rollback behaviour to the app state in case the tx will never
// get broadcast.
log.warn("TxBroadcaster.onTimeout called: {}", exception.toString());
onSuccess(tx);
} else {
log.error("TxBroadcaster.onTimeout: Tx is null. exception={} ", exception.toString());
onFailure(exception);
}
}
void onFailure(TxBroadcastException exception);
}
// Currently there is a bug in BitcoinJ causing the timeout at all BSQ transactions.
// It is because BitcoinJ does not handle confidence object correctly in case as tx got altered after the
// Wallet.complete() method is called which is the case for all BSQ txs. We will work on a fix for that but that
// will take more time. In the meantime we reduce the timeout to 5 seconds to avoid that the trade protocol runs
// into a timeout when using BSQ for trade fee.
// For trade fee txs we set only 1 sec timeout for now.
// FIXME
private static final int DEFAULT_BROADCAST_TIMEOUT = 5;
private static final Map<String, Timer> broadcastTimerMap = new HashMap<>();
public static void broadcastTx(Wallet wallet, PeerGroup peerGroup, Transaction localTx, Callback callback) {
broadcastTx(wallet, peerGroup, localTx, callback, DEFAULT_BROADCAST_TIMEOUT);
}
public static void broadcastTx(Wallet wallet, PeerGroup peerGroup, Transaction tx, Callback callback, int timeOut) {
Timer timeoutTimer;
final String txId = tx.getTxId().toString();
log.info("Txid: {} hex: {}", txId, Utils.HEX.encode(tx.bitcoinSerialize()));
if (!broadcastTimerMap.containsKey(txId)) {
timeoutTimer = UserThread.runAfter(() -> {
log.warn("Broadcast of tx {} not completed after {} sec.", txId, timeOut);
stopAndRemoveTimer(txId);
UserThread.execute(() -> callback.onTimeout(new TxBroadcastTimeoutException(tx, timeOut, wallet)));
}, timeOut);
broadcastTimerMap.put(txId, timeoutTimer);
} else {
// Would be the wrong way how to use the API (calling 2 times a broadcast with same tx).
// An arbitrator reported that got the error after a manual payout, need to investigate why...
stopAndRemoveTimer(txId);
UserThread.execute(() -> callback.onFailure(new TxBroadcastException("We got broadcastTx called with a tx " +
"which has an open timeoutTimer. txId=" + txId, txId)));
}
// We decided the least risky scenario is to commit the tx to the wallet and broadcast it later.
// If it's a bsq tx WalletManager.publishAndCommitBsqTx() should have committed the tx to both bsq and btc
// wallets so the next line causes no effect.
// If it's a btc tx, the next line adds the tx to the wallet.
wallet.maybeCommitTx(tx);
Futures.addCallback(peerGroup.broadcastTransaction(tx).future(), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Transaction result) {
// We expect that there is still a timeout in our map, otherwise the timeout got triggered
if (broadcastTimerMap.containsKey(txId)) {
stopAndRemoveTimer(txId);
// At regtest we get called immediately back but we want to make sure that the handler is not called
// before the caller is finished.
UserThread.execute(() -> callback.onSuccess(tx));
} else {
log.warn("We got an onSuccess callback for a broadcast which already triggered the timeout. txId={}", txId);
}
}
@Override
public void onFailure(@NotNull Throwable throwable) {
stopAndRemoveTimer(txId);
UserThread.execute(() -> callback.onFailure(new TxBroadcastException("We got an onFailure from " +
"the peerGroup.broadcastTransaction callback.", throwable)));
}
}, MoreExecutors.directExecutor());
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(tx);
}
private static void stopAndRemoveTimer(String txId) {
Timer timer = broadcastTimerMap.get(txId);
if (timer != null)
timer.stop();
broadcastTimerMap.remove(txId);
}
}

View file

@ -21,12 +21,7 @@ import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.SetMultimap;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;
import haveno.common.config.Config;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.handlers.ResultHandler;
import haveno.core.user.Preferences;
import haveno.core.xmr.exceptions.TransactionVerificationException;
import haveno.core.xmr.exceptions.WalletException;
@ -34,7 +29,6 @@ import haveno.core.xmr.listeners.AddressConfidenceListener;
import haveno.core.xmr.listeners.BalanceListener;
import haveno.core.xmr.listeners.TxConfidenceListener;
import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import lombok.Getter;
@ -42,12 +36,10 @@ import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
@ -73,7 +65,6 @@ import org.bitcoinj.wallet.DecryptingKeyBag;
import org.bitcoinj.wallet.DeterministicSeed;
import org.bitcoinj.wallet.KeyBag;
import org.bitcoinj.wallet.RedeemData;
import org.bitcoinj.wallet.SendRequest;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.listeners.WalletChangeEventListener;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
@ -364,19 +355,6 @@ public abstract class WalletService {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Broadcast tx
///////////////////////////////////////////////////////////////////////////////////////////
public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback) {
TxBroadcaster.broadcastTx(wallet, walletsSetup.getPeerGroup(), tx, callback);
}
public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback, int timeOut) {
TxBroadcaster.broadcastTx(wallet, walletsSetup.getPeerGroup(), tx, callback, timeOut);
}
///////////////////////////////////////////////////////////////////////////////////////////
// TransactionConfidence
///////////////////////////////////////////////////////////////////////////////////////////
@ -543,41 +521,6 @@ public abstract class WalletService {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Empty complete Wallet
///////////////////////////////////////////////////////////////////////////////////////////
public void emptyBtcWallet(String toAddress,
KeyParameter aesKey,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler)
throws InsufficientMoneyException, AddressFormatException {
SendRequest sendRequest = SendRequest.emptyWallet(Address.fromString(params, toAddress));
sendRequest.fee = Coin.ZERO;
sendRequest.aesKey = aesKey;
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
printTx("empty btc wallet", sendResult.tx);
// For better redundancy in case the broadcast via BitcoinJ fails we also
// publish the tx via mempool nodes.
MemPoolSpaceTxBroadcaster.broadcastTx(sendResult.tx);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(Transaction result) {
log.info("emptyBtcWallet onSuccess Transaction=" + result);
resultHandler.handleResult();
}
@Override
public void onFailure(@NotNull Throwable t) {
log.error("emptyBtcWallet onFailure " + t.toString());
errorMessageHandler.handleErrorMessage(t.getMessage());
}
}, MoreExecutors.directExecutor());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -449,7 +449,7 @@ public class XmrWalletService {
// verify miner fee
BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue();
if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee());
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);

View file

@ -1,150 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.xmr.wallet.http;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import haveno.common.app.Version;
import haveno.common.config.Config;
import haveno.common.util.Utilities;
import haveno.core.user.Preferences;
import haveno.core.xmr.nodes.LocalBitcoinNode;
import haveno.network.Socks5ProxyProvider;
import haveno.network.http.HttpException;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Utils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class MemPoolSpaceTxBroadcaster {
private static Socks5ProxyProvider socks5ProxyProvider;
private static Preferences preferences;
private static LocalBitcoinNode localBitcoinNode;
private static final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
"MemPoolSpaceTxBroadcaster", 3, 5, 10 * 60);
public static void init(Socks5ProxyProvider socks5ProxyProvider,
Preferences preferences,
LocalBitcoinNode localBitcoinNode) {
MemPoolSpaceTxBroadcaster.socks5ProxyProvider = socks5ProxyProvider;
MemPoolSpaceTxBroadcaster.preferences = preferences;
MemPoolSpaceTxBroadcaster.localBitcoinNode = localBitcoinNode;
}
public static void broadcastTx(Transaction tx) {
if (!Config.baseCurrencyNetwork().isMainnet()) {
log.info("MemPoolSpaceTxBroadcaster only supports mainnet");
return;
}
if (localBitcoinNode.shouldBeUsed()) {
log.info("A localBitcoinNode is detected and used. For privacy reasons we do not use the tx " +
"broadcast to mempool nodes in that case.");
return;
}
if (socks5ProxyProvider == null) {
log.warn("We got broadcastTx called before init was called.");
return;
}
String txIdToSend = tx.getTxId().toString();
String rawTx = Utils.HEX.encode(tx.bitcoinSerialize(true));
List<String> txBroadcastServices = new ArrayList<>(preferences.getDefaultTxBroadcastServices());
// Broadcast to first service
String serviceAddress = broadcastTx(txIdToSend, rawTx, txBroadcastServices);
if (serviceAddress != null) {
// Broadcast to second service
txBroadcastServices.remove(serviceAddress);
broadcastTx(txIdToSend, rawTx, txBroadcastServices);
}
}
@Nullable
private static String broadcastTx(String txIdToSend, String rawTx, List<String> txBroadcastServices) {
String serviceAddress = getRandomServiceAddress(txBroadcastServices);
if (serviceAddress == null) {
log.warn("We don't have a serviceAddress available. txBroadcastServices={}", txBroadcastServices);
return null;
}
broadcastTx(serviceAddress, txIdToSend, rawTx);
return serviceAddress;
}
private static void broadcastTx(String serviceAddress, String txIdToSend, String rawTx) {
TxBroadcastHttpClient httpClient = new TxBroadcastHttpClient(socks5ProxyProvider);
httpClient.setBaseUrl(serviceAddress);
httpClient.setIgnoreSocks5Proxy(false);
log.info("We broadcast rawTx {} to {}", rawTx, serviceAddress);
ListenableFuture<String> future = executorService.submit(() -> {
Thread.currentThread().setName("MemPoolSpaceTxBroadcaster @ " + serviceAddress);
return httpClient.post(rawTx, "User-Agent", "haveno/" + Version.VERSION);
});
Futures.addCallback(future, new FutureCallback<>() {
public void onSuccess(String txId) {
if (txId.equals(txIdToSend)) {
log.info("Broadcast of raw tx with txId {} to {} was successful. rawTx={}",
txId, serviceAddress, rawTx);
} else {
log.error("The txId we got returned from the service does not match " +
"out tx of the sending tx. txId={}; txIdToSend={}",
txId, txIdToSend);
}
}
public void onFailure(@NotNull Throwable throwable) {
Throwable cause = throwable.getCause();
if (cause instanceof HttpException) {
int responseCode = ((HttpException) cause).getResponseCode();
String message = cause.getMessage();
// See all error codes at: https://github.com/bitcoin/bitcoin/blob/master/src/rpc/protocol.h
if (responseCode == 400 && message.contains("code\":-27")) {
log.info("Broadcast of raw tx to {} failed as transaction {} is already confirmed",
serviceAddress, txIdToSend);
} else {
log.info("Broadcast of raw tx to {} failed for transaction {}. responseCode={}, error={}",
serviceAddress, txIdToSend, responseCode, message);
}
} else {
log.warn("Broadcast of raw tx with txId {} to {} failed. Error={}",
txIdToSend, serviceAddress, throwable.toString());
}
}
}, MoreExecutors.directExecutor());
}
@Nullable
private static String getRandomServiceAddress(List<String> txBroadcastServices) {
List<String> list = checkNotNull(txBroadcastServices);
return !list.isEmpty() ? list.get(new Random().nextInt(list.size())) : null;
}
}

View file

@ -1,30 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.xmr.wallet.http;
import haveno.core.trade.txproof.AssetTxProofHttpClient;
import haveno.network.Socks5ProxyProvider;
import haveno.network.http.HttpClientImpl;
import lombok.extern.slf4j.Slf4j;
@Slf4j
class TxBroadcastHttpClient extends HttpClientImpl implements AssetTxProofHttpClient {
TxBroadcastHttpClient(Socks5ProxyProvider socks5ProxyProvider) {
super(socks5ProxyProvider);
}
}