mirror of
https://github.com/boldsuck/haveno.git
synced 2024-12-23 04:29:22 +00:00
remove XmrTxProofService
This commit is contained in:
parent
33147e1c7c
commit
37e812dead
38 changed files with 5 additions and 3737 deletions
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
package haveno.core.api;
|
package haveno.core.api;
|
||||||
|
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
|
||||||
import haveno.common.app.Version;
|
import haveno.common.app.Version;
|
||||||
import haveno.common.config.Config;
|
import haveno.common.config.Config;
|
||||||
import haveno.common.crypto.IncorrectPasswordException;
|
import haveno.common.crypto.IncorrectPasswordException;
|
||||||
|
@ -296,14 +295,6 @@ public class CoreApi {
|
||||||
return walletsService.getFundingAddresses();
|
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) {
|
public Transaction getTransaction(String txId) {
|
||||||
return walletsService.getTransaction(txId);
|
return walletsService.getTransaction(txId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ package haveno.core.api;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
|
||||||
import haveno.common.Timer;
|
import haveno.common.Timer;
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.core.api.model.AddressBalanceInfo;
|
import haveno.core.api.model.AddressBalanceInfo;
|
||||||
|
@ -32,8 +31,6 @@ import haveno.core.user.Preferences;
|
||||||
import haveno.core.util.FormattingUtils;
|
import haveno.core.util.FormattingUtils;
|
||||||
import haveno.core.util.coin.CoinFormatter;
|
import haveno.core.util.coin.CoinFormatter;
|
||||||
import haveno.core.xmr.Balances;
|
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.model.AddressEntry;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
|
@ -44,7 +41,6 @@ import monero.wallet.model.MoneroDestination;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.bitcoinj.core.TransactionConfidence;
|
import org.bitcoinj.core.TransactionConfidence;
|
||||||
|
@ -57,7 +53,6 @@ import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -229,61 +224,6 @@ class CoreWalletsService {
|
||||||
.collect(Collectors.toList());
|
.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) {
|
Transaction getTransaction(String txId) {
|
||||||
if (txId.length() != 64)
|
if (txId.length() != 64)
|
||||||
throw new IllegalArgumentException(format("%s is not a transaction id", txId));
|
throw new IllegalArgumentException(format("%s is not a transaction id", txId));
|
||||||
|
|
|
@ -47,7 +47,6 @@ import haveno.core.trade.ClosedTradableManager;
|
||||||
import haveno.core.trade.TradeManager;
|
import haveno.core.trade.TradeManager;
|
||||||
import haveno.core.trade.failed.FailedTradesManager;
|
import haveno.core.trade.failed.FailedTradesManager;
|
||||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||||
import haveno.core.trade.txproof.xmr.XmrTxProofService;
|
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
import haveno.core.xmr.Balances;
|
import haveno.core.xmr.Balances;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
|
@ -72,7 +71,6 @@ public class DomainInitialisation {
|
||||||
private final TradeManager tradeManager;
|
private final TradeManager tradeManager;
|
||||||
private final ClosedTradableManager closedTradableManager;
|
private final ClosedTradableManager closedTradableManager;
|
||||||
private final FailedTradesManager failedTradesManager;
|
private final FailedTradesManager failedTradesManager;
|
||||||
private final XmrTxProofService xmrTxProofService;
|
|
||||||
private final OpenOfferManager openOfferManager;
|
private final OpenOfferManager openOfferManager;
|
||||||
private final Balances balances;
|
private final Balances balances;
|
||||||
private final WalletAppSetup walletAppSetup;
|
private final WalletAppSetup walletAppSetup;
|
||||||
|
@ -105,7 +103,6 @@ public class DomainInitialisation {
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
ClosedTradableManager closedTradableManager,
|
ClosedTradableManager closedTradableManager,
|
||||||
FailedTradesManager failedTradesManager,
|
FailedTradesManager failedTradesManager,
|
||||||
XmrTxProofService xmrTxProofService,
|
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
Balances balances,
|
Balances balances,
|
||||||
WalletAppSetup walletAppSetup,
|
WalletAppSetup walletAppSetup,
|
||||||
|
@ -136,7 +133,6 @@ public class DomainInitialisation {
|
||||||
this.tradeManager = tradeManager;
|
this.tradeManager = tradeManager;
|
||||||
this.closedTradableManager = closedTradableManager;
|
this.closedTradableManager = closedTradableManager;
|
||||||
this.failedTradesManager = failedTradesManager;
|
this.failedTradesManager = failedTradesManager;
|
||||||
this.xmrTxProofService = xmrTxProofService;
|
|
||||||
this.openOfferManager = openOfferManager;
|
this.openOfferManager = openOfferManager;
|
||||||
this.balances = balances;
|
this.balances = balances;
|
||||||
this.walletAppSetup = walletAppSetup;
|
this.walletAppSetup = walletAppSetup;
|
||||||
|
@ -182,7 +178,6 @@ public class DomainInitialisation {
|
||||||
|
|
||||||
closedTradableManager.onAllServicesInitialized();
|
closedTradableManager.onAllServicesInitialized();
|
||||||
failedTradesManager.onAllServicesInitialized();
|
failedTradesManager.onAllServicesInitialized();
|
||||||
xmrTxProofService.onAllServicesInitialized();
|
|
||||||
|
|
||||||
openOfferManager.onAllServicesInitialized();
|
openOfferManager.onAllServicesInitialized();
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,6 @@ import haveno.core.setup.CoreSetup;
|
||||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||||
import haveno.core.trade.txproof.xmr.XmrTxProofService;
|
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
|
@ -343,7 +342,6 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||||
injector.getInstance(PriceFeedService.class).shutDown();
|
injector.getInstance(PriceFeedService.class).shutDown();
|
||||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||||
injector.getInstance(XmrTxProofService.class).shutDown();
|
|
||||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||||
|
|
||||||
// shut down open offer manager
|
// shut down open offer manager
|
||||||
|
|
|
@ -57,7 +57,6 @@ import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
import haveno.core.xmr.wallet.WalletsManager;
|
import haveno.core.xmr.wallet.WalletsManager;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
|
|
||||||
import haveno.network.Socks5ProxyProvider;
|
import haveno.network.Socks5ProxyProvider;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
|
@ -248,8 +247,6 @@ public class HavenoSetup {
|
||||||
this.arbitrationManager = arbitrationManager;
|
this.arbitrationManager = arbitrationManager;
|
||||||
|
|
||||||
HavenoUtils.havenoSetup = this;
|
HavenoUtils.havenoSetup = this;
|
||||||
|
|
||||||
MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -431,7 +431,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
}
|
}
|
||||||
if (feeEstimateTx != null) {
|
if (feeEstimateTx != null) {
|
||||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
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());
|
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);
|
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,6 @@ import haveno.core.trade.protocol.ProcessModelServiceProvider;
|
||||||
import haveno.core.trade.protocol.TradeListener;
|
import haveno.core.trade.protocol.TradeListener;
|
||||||
import haveno.core.trade.protocol.TradePeer;
|
import haveno.core.trade.protocol.TradePeer;
|
||||||
import haveno.core.trade.protocol.TradeProtocol;
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import haveno.core.trade.txproof.AssetTxProofResult;
|
|
||||||
import haveno.core.util.VolumeUtil;
|
import haveno.core.util.VolumeUtil;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
|
@ -55,13 +54,11 @@ import haveno.network.p2p.AckMessage;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.IntegerProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyStringProperty;
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
|
@ -427,18 +424,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
@Setter
|
@Setter
|
||||||
private String counterCurrencyExtraData;
|
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
|
// Added in XMR integration
|
||||||
private transient List<TradeListener> tradeListeners; // notified on fully validated trade messages
|
private transient List<TradeListener> tradeListeners; // notified on fully validated trade messages
|
||||||
transient MoneroWalletListener depositTxListener;
|
transient MoneroWalletListener depositTxListener;
|
||||||
|
@ -1342,11 +1327,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
errorMessageProperty.set(appendedErrorMessage);
|
errorMessageProperty.set(appendedErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) {
|
|
||||||
this.assetTxProofResult = assetTxProofResult;
|
|
||||||
assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Getter
|
// Getter
|
||||||
|
@ -1996,7 +1976,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
||||||
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey));
|
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey));
|
||||||
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
||||||
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2020,13 +1999,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
trade.setStartTime(proto.getStartTime());
|
trade.setStartTime(proto.getStartTime());
|
||||||
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
|
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()
|
trade.chatMessages.addAll(proto.getChatMessageList().stream()
|
||||||
.map(ChatMessage::fromPayloadProto)
|
.map(ChatMessage::fromPayloadProto)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
|
@ -2055,7 +2027,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
",\n errorMessage='" + errorMessage + '\'' +
|
",\n errorMessage='" + errorMessage + '\'' +
|
||||||
",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' +
|
",\n counterCurrencyTxId='" + counterCurrencyTxId + '\'' +
|
||||||
",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' +
|
",\n counterCurrencyExtraData='" + counterCurrencyExtraData + '\'' +
|
||||||
",\n assetTxProofResult='" + assetTxProofResult + '\'' +
|
|
||||||
",\n chatMessages=" + chatMessages +
|
",\n chatMessages=" + chatMessages +
|
||||||
",\n totalTxFee=" + totalTxFee +
|
",\n totalTxFee=" + totalTxFee +
|
||||||
",\n takerFee=" + takerFee +
|
",\n takerFee=" + takerFee +
|
||||||
|
|
|
@ -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 {
|
|
||||||
}
|
|
|
@ -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 {
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -18,10 +18,6 @@
|
||||||
package haveno.core.xmr.wallet;
|
package haveno.core.xmr.wallet;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
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.common.util.Tuple2;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.xmr.exceptions.AddressEntryException;
|
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.AddressEntry;
|
||||||
import haveno.core.xmr.model.AddressEntryList;
|
import haveno.core.xmr.model.AddressEntryList;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
|
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
import org.bitcoinj.core.AddressFormatException;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
@ -39,16 +34,13 @@ import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
import org.bitcoinj.core.InsufficientMoneyException;
|
||||||
import org.bitcoinj.core.SegwitAddress;
|
import org.bitcoinj.core.SegwitAddress;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.bitcoinj.core.TransactionConfidence;
|
|
||||||
import org.bitcoinj.core.TransactionInput;
|
import org.bitcoinj.core.TransactionInput;
|
||||||
import org.bitcoinj.core.TransactionOutPoint;
|
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
import org.bitcoinj.crypto.DeterministicKey;
|
import org.bitcoinj.crypto.DeterministicKey;
|
||||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||||
import org.bitcoinj.script.Script;
|
import org.bitcoinj.script.Script;
|
||||||
import org.bitcoinj.script.ScriptPattern;
|
import org.bitcoinj.script.ScriptPattern;
|
||||||
import org.bitcoinj.wallet.SendRequest;
|
import org.bitcoinj.wallet.SendRequest;
|
||||||
import org.bitcoinj.wallet.Wallet;
|
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.slf4j.Logger;
|
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
|
// Withdrawal Fee calculation
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -701,54 +537,6 @@ public class BtcWalletService extends WalletService {
|
||||||
// Withdrawal Send
|
// 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,
|
private SendRequest getSendRequest(String fromAddress,
|
||||||
String toAddress,
|
String toAddress,
|
||||||
Coin amount,
|
Coin amount,
|
||||||
|
|
|
@ -994,30 +994,6 @@ public class TradeWalletService {
|
||||||
return new Tuple2<>(txId, signedTxHex);
|
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
|
// Misc
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -21,12 +21,7 @@ import com.google.common.collect.ImmutableMultiset;
|
||||||
import com.google.common.collect.ImmutableSetMultimap;
|
import com.google.common.collect.ImmutableSetMultimap;
|
||||||
import com.google.common.collect.Multiset;
|
import com.google.common.collect.Multiset;
|
||||||
import com.google.common.collect.SetMultimap;
|
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.config.Config;
|
||||||
import haveno.common.handlers.ErrorMessageHandler;
|
|
||||||
import haveno.common.handlers.ResultHandler;
|
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.xmr.exceptions.TransactionVerificationException;
|
import haveno.core.xmr.exceptions.TransactionVerificationException;
|
||||||
import haveno.core.xmr.exceptions.WalletException;
|
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.BalanceListener;
|
||||||
import haveno.core.xmr.listeners.TxConfidenceListener;
|
import haveno.core.xmr.listeners.TxConfidenceListener;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.http.MemPoolSpaceTxBroadcaster;
|
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -42,12 +36,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
|
||||||
import org.bitcoinj.core.BlockChain;
|
import org.bitcoinj.core.BlockChain;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.Context;
|
import org.bitcoinj.core.Context;
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Sha256Hash;
|
import org.bitcoinj.core.Sha256Hash;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
|
@ -73,7 +65,6 @@ import org.bitcoinj.wallet.DecryptingKeyBag;
|
||||||
import org.bitcoinj.wallet.DeterministicSeed;
|
import org.bitcoinj.wallet.DeterministicSeed;
|
||||||
import org.bitcoinj.wallet.KeyBag;
|
import org.bitcoinj.wallet.KeyBag;
|
||||||
import org.bitcoinj.wallet.RedeemData;
|
import org.bitcoinj.wallet.RedeemData;
|
||||||
import org.bitcoinj.wallet.SendRequest;
|
|
||||||
import org.bitcoinj.wallet.Wallet;
|
import org.bitcoinj.wallet.Wallet;
|
||||||
import org.bitcoinj.wallet.listeners.WalletChangeEventListener;
|
import org.bitcoinj.wallet.listeners.WalletChangeEventListener;
|
||||||
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
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
|
// 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
|
// Getters
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -449,7 +449,7 @@ public class XmrWalletService {
|
||||||
|
|
||||||
// verify miner fee
|
// verify miner fee
|
||||||
BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
|
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());
|
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);
|
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,176 +0,0 @@
|
||||||
package haveno.core.trade.txproof.xmr;
|
|
||||||
|
|
||||||
import haveno.core.user.AutoConfirmSettings;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import static haveno.core.trade.txproof.xmr.XmrTxProofParser.MAX_DATE_TOLERANCE;
|
|
||||||
import static org.junit.Assert.assertNotSame;
|
|
||||||
import static org.junit.Assert.assertSame;
|
|
||||||
|
|
||||||
public class XmrTxProofParserTest {
|
|
||||||
private XmrTxProofModel xmrTxProofModel;
|
|
||||||
private String recipientAddressHex = "e957dac72bcec80d59b2fecacfa7522223b6a5df895b7e388e60297e85f3f867b42f43e8d9f086a99a997704ceb92bd9cd99d33952de90c9f5f93c82c62360ae";
|
|
||||||
private String txHash = "488e48ab0c7e69028d19f787ec57fd496ff114caba9ab265bfd41a3ea0e4687d";
|
|
||||||
private String txKey = "6c336e52ed537676968ee319af6983c80b869ca6a732b5962c02748b486f8f0f";
|
|
||||||
private XmrTxProofParser parser;
|
|
||||||
private Date tradeDate;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void prepareMocksAndObjects() {
|
|
||||||
long amount = 100000000000L;
|
|
||||||
tradeDate = new Date(1574922644000L);
|
|
||||||
String serviceAddress = "127.0.0.1:8081";
|
|
||||||
AutoConfirmSettings autoConfirmSettings = new AutoConfirmSettings(true,
|
|
||||||
10,
|
|
||||||
1,
|
|
||||||
Collections.singletonList(serviceAddress),
|
|
||||||
"XMR");
|
|
||||||
|
|
||||||
// TODO using the mocking framework would be better...
|
|
||||||
String recipientAddress = "4ATyxmFGU7h3EWu5kYR6gy6iCNFCftbsjATfbuBBjsRHJM4KTwEyeiyVNNUmsfpK1kdRxs8QoPLsZanGqe1Mby43LeyWNMF";
|
|
||||||
xmrTxProofModel = new XmrTxProofModel(
|
|
||||||
"dummyTest",
|
|
||||||
txHash,
|
|
||||||
txKey,
|
|
||||||
recipientAddress,
|
|
||||||
amount,
|
|
||||||
tradeDate,
|
|
||||||
autoConfirmSettings);
|
|
||||||
|
|
||||||
parser = new XmrTxProofParser();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonRoot() {
|
|
||||||
// checking what happens when bad input is provided
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"invalid json data").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"[]").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"{}").getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonTopLevel() {
|
|
||||||
// testing the top level fields: data and status
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"{'data':{'title':''},'status':'fail'}")
|
|
||||||
.getDetail(), XmrTxProofRequest.Detail.TX_NOT_FOUND);
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"{'data':{'title':''},'missingstatus':'success'}")
|
|
||||||
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"{'missingdata':{'title':''},'status':'success'}")
|
|
||||||
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonAddress() {
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"{'data':{'missingaddress':'irrelevant'},'status':'success'}")
|
|
||||||
.getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
assertSame(parser.parse(xmrTxProofModel,
|
|
||||||
"{'data':{'address':'e957dac7'},'status':'success'}")
|
|
||||||
.getDetail(), XmrTxProofRequest.Detail.ADDRESS_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonTxHash() {
|
|
||||||
String missing_tx_hash = "{'data':{'address':'" + recipientAddressHex + "'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, missing_tx_hash).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
|
|
||||||
String invalid_tx_hash = "{'data':{'address':'" + recipientAddressHex + "', 'tx_hash':'488e48'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, invalid_tx_hash).getDetail(), XmrTxProofRequest.Detail.TX_HASH_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonTxKey() {
|
|
||||||
String missing_tx_key = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, missing_tx_key).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
|
|
||||||
String invalid_tx_key = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "', " +
|
|
||||||
"'viewkey':'cdce04'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, invalid_tx_key).getDetail(), XmrTxProofRequest.Detail.TX_KEY_INVALID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonTxTimestamp() {
|
|
||||||
String missing_tx_timestamp = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "'," +
|
|
||||||
"'viewkey':'" + txKey + "'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, missing_tx_timestamp).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
|
|
||||||
String invalid_tx_timestamp = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "', " +
|
|
||||||
"'viewkey':'" + txKey + "'," +
|
|
||||||
"'tx_timestamp':'12345'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, invalid_tx_timestamp).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING);
|
|
||||||
|
|
||||||
long tradeTimeSec = tradeDate.getTime() / 1000;
|
|
||||||
String ts = String.valueOf(tradeTimeSec - MAX_DATE_TOLERANCE - 1);
|
|
||||||
String invalid_tx_timestamp_1ms_too_old = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "', " +
|
|
||||||
"'viewkey':'" + txKey + "'," +
|
|
||||||
"'tx_timestamp':'" + ts + "'}, 'status':'success'}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, invalid_tx_timestamp_1ms_too_old).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING);
|
|
||||||
|
|
||||||
ts = String.valueOf(tradeTimeSec - MAX_DATE_TOLERANCE);
|
|
||||||
String valid_tx_timestamp_exact_MAX_DATE_TOLERANCE = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "', " +
|
|
||||||
"'viewkey':'" + txKey + "'," +
|
|
||||||
"'tx_timestamp':'" + ts + "'}, 'status':'success'}";
|
|
||||||
parser.parse(xmrTxProofModel, valid_tx_timestamp_exact_MAX_DATE_TOLERANCE);
|
|
||||||
assertNotSame(parser.parse(xmrTxProofModel, valid_tx_timestamp_exact_MAX_DATE_TOLERANCE).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING);
|
|
||||||
|
|
||||||
ts = String.valueOf(tradeTimeSec - MAX_DATE_TOLERANCE + 1);
|
|
||||||
String valid_tx_timestamp_less_than_MAX_DATE_TOLERANCE = "{'data':{'address':'" + recipientAddressHex + "', " +
|
|
||||||
"'tx_hash':'" + txHash + "', " +
|
|
||||||
"'viewkey':'" + txKey + "'," +
|
|
||||||
"'tx_timestamp':'" + ts + "'}, 'status':'success'}";
|
|
||||||
assertNotSame(parser.parse(xmrTxProofModel, valid_tx_timestamp_less_than_MAX_DATE_TOLERANCE).getDetail(), XmrTxProofRequest.Detail.TRADE_DATE_NOT_MATCHING);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonTxConfirmation() {
|
|
||||||
long epochDate = Instant.now().toEpochMilli() / 1000;
|
|
||||||
String outputs = "'outputs':[" +
|
|
||||||
"{'amount':100000000000,'match':true,'output_idx':0,'output_pubkey':'972a2c9178876f1fae4ecd22f9d7c132a12706db8ffb5d1f223f9aa8ced75b61'}," +
|
|
||||||
"{'amount':0,'match':false,'output_idx':1,'output_pubkey':'658330d2d56c74aca3b40900c56cd0f0111e2876be677ade493d06d539a1bab0'}],";
|
|
||||||
String json = "{'status':'success', 'data':{" +
|
|
||||||
"'address':'" + recipientAddressHex + "', " +
|
|
||||||
outputs +
|
|
||||||
"'tx_confirmations':777, " +
|
|
||||||
"'tx_hash':'" + txHash + "', " +
|
|
||||||
"'viewkey':'" + txKey + "', " +
|
|
||||||
"'tx_timestamp':'" + epochDate + "'}" +
|
|
||||||
"}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, json), XmrTxProofRequest.Result.SUCCESS);
|
|
||||||
json = json.replaceFirst("777", "0");
|
|
||||||
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, json).getDetail(), XmrTxProofRequest.Detail.PENDING_CONFIRMATIONS);
|
|
||||||
|
|
||||||
json = json.replaceFirst("100000000000", "100000000001");
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, json).getDetail(), XmrTxProofRequest.Detail.AMOUNT_NOT_MATCHING);
|
|
||||||
|
|
||||||
// Revert change of amount
|
|
||||||
json = json.replaceFirst("100000000001", "100000000000");
|
|
||||||
json = json.replaceFirst("'match':true", "'match':false");
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, json).getDetail(), XmrTxProofRequest.Detail.NO_MATCH_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testJsonFail() {
|
|
||||||
String failedJson = "{\"data\":null,\"message\":\"Cant parse tx hash: a\",\"status\":\"error\"}";
|
|
||||||
assertSame(parser.parse(xmrTxProofModel, failedJson).getDetail(), XmrTxProofRequest.Detail.API_INVALID);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,7 +36,6 @@ import haveno.core.user.Cookie;
|
||||||
import haveno.core.user.CookieKey;
|
import haveno.core.user.CookieKey;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
|
||||||
import haveno.core.xmr.wallet.WalletsManager;
|
import haveno.core.xmr.wallet.WalletsManager;
|
||||||
import haveno.desktop.common.view.CachingViewLoader;
|
import haveno.desktop.common.view.CachingViewLoader;
|
||||||
import haveno.desktop.common.view.View;
|
import haveno.desktop.common.view.View;
|
||||||
|
@ -44,9 +43,7 @@ import haveno.desktop.common.view.ViewLoader;
|
||||||
import haveno.desktop.main.MainView;
|
import haveno.desktop.main.MainView;
|
||||||
import haveno.desktop.main.debug.DebugView;
|
import haveno.desktop.main.debug.DebugView;
|
||||||
import haveno.desktop.main.overlays.popups.Popup;
|
import haveno.desktop.main.overlays.popups.Popup;
|
||||||
import haveno.desktop.main.overlays.windows.BtcEmptyWalletWindow;
|
|
||||||
import haveno.desktop.main.overlays.windows.FilterWindow;
|
import haveno.desktop.main.overlays.windows.FilterWindow;
|
||||||
import haveno.desktop.main.overlays.windows.ManualPayoutTxWindow;
|
|
||||||
import haveno.desktop.main.overlays.windows.SendAlertMessageWindow;
|
import haveno.desktop.main.overlays.windows.SendAlertMessageWindow;
|
||||||
import haveno.desktop.main.overlays.windows.ShowWalletDataWindow;
|
import haveno.desktop.main.overlays.windows.ShowWalletDataWindow;
|
||||||
import haveno.desktop.util.CssTheme;
|
import haveno.desktop.util.CssTheme;
|
||||||
|
@ -299,9 +296,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler {
|
||||||
Utilities.isCtrlPressed(KeyCode.Q, keyEvent)) {
|
Utilities.isCtrlPressed(KeyCode.Q, keyEvent)) {
|
||||||
shutDownByUser();
|
shutDownByUser();
|
||||||
} else {
|
} else {
|
||||||
if (Utilities.isAltOrCtrlPressed(KeyCode.E, keyEvent)) {
|
if (Utilities.isAltOrCtrlPressed(KeyCode.M, keyEvent)) {
|
||||||
injector.getInstance(BtcEmptyWalletWindow.class).show();
|
|
||||||
} else if (Utilities.isAltOrCtrlPressed(KeyCode.M, keyEvent)) {
|
|
||||||
injector.getInstance(SendAlertMessageWindow.class).show();
|
injector.getInstance(SendAlertMessageWindow.class).show();
|
||||||
} else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) {
|
} else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) {
|
||||||
injector.getInstance(FilterWindow.class).show();
|
injector.getInstance(FilterWindow.class).show();
|
||||||
|
@ -323,11 +318,6 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler {
|
||||||
new ShowWalletDataWindow(walletsManager).show();
|
new ShowWalletDataWindow(walletsManager).show();
|
||||||
else
|
else
|
||||||
new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show();
|
new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show();
|
||||||
} else if (Utilities.isAltOrCtrlPressed(KeyCode.G, keyEvent)) {
|
|
||||||
if (injector.getInstance(BtcWalletService.class).isWalletReady())
|
|
||||||
injector.getInstance(ManualPayoutTxWindow.class).show();
|
|
||||||
else
|
|
||||||
new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show();
|
|
||||||
} else if (DevEnv.isDevMode()) {
|
} else if (DevEnv.isDevMode()) {
|
||||||
if (Utilities.isAltOrCtrlPressed(KeyCode.Z, keyEvent))
|
if (Utilities.isAltOrCtrlPressed(KeyCode.Z, keyEvent))
|
||||||
showDebugWindow(scene, injector);
|
showDebugWindow(scene, injector);
|
||||||
|
|
|
@ -1,170 +0,0 @@
|
||||||
package haveno.desktop.main.overlays.windows;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import haveno.common.UserThread;
|
|
||||||
import haveno.core.api.CoreMoneroConnectionsService;
|
|
||||||
import haveno.core.locale.Res;
|
|
||||||
import haveno.core.offer.OpenOfferManager;
|
|
||||||
import haveno.core.util.FormattingUtils;
|
|
||||||
import haveno.core.util.coin.CoinFormatter;
|
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
|
||||||
import haveno.core.xmr.wallet.Restrictions;
|
|
||||||
import haveno.desktop.components.AutoTooltipButton;
|
|
||||||
import haveno.desktop.components.InputTextField;
|
|
||||||
import haveno.desktop.main.overlays.Overlay;
|
|
||||||
import haveno.desktop.main.overlays.popups.Popup;
|
|
||||||
import haveno.desktop.util.GUIUtil;
|
|
||||||
import haveno.desktop.util.Transitions;
|
|
||||||
import haveno.network.p2p.P2PService;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.input.KeyCode;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static haveno.desktop.util.FormBuilder.addInputTextField;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addMultilineLabel;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addTopLabelTextField;
|
|
||||||
|
|
||||||
public final class BtcEmptyWalletWindow extends Overlay<BtcEmptyWalletWindow> {
|
|
||||||
protected static final Logger log = LoggerFactory.getLogger(BtcEmptyWalletWindow.class);
|
|
||||||
|
|
||||||
private final WalletPasswordWindow walletPasswordWindow;
|
|
||||||
private final OpenOfferManager openOfferManager;
|
|
||||||
private final P2PService p2PService;
|
|
||||||
private final CoreMoneroConnectionsService connectionService;
|
|
||||||
private final BtcWalletService btcWalletService;
|
|
||||||
private final CoinFormatter btcFormatter;
|
|
||||||
|
|
||||||
private Button emptyWalletButton;
|
|
||||||
private InputTextField addressInputTextField;
|
|
||||||
private TextField balanceTextField;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public BtcEmptyWalletWindow(WalletPasswordWindow walletPasswordWindow,
|
|
||||||
OpenOfferManager openOfferManager,
|
|
||||||
P2PService p2PService,
|
|
||||||
CoreMoneroConnectionsService connectionService,
|
|
||||||
BtcWalletService btcWalletService,
|
|
||||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) {
|
|
||||||
headLine(Res.get("emptyWalletWindow.headline", "BTC"));
|
|
||||||
width = 768;
|
|
||||||
type = Type.Instruction;
|
|
||||||
|
|
||||||
this.p2PService = p2PService;
|
|
||||||
this.connectionService = connectionService;
|
|
||||||
this.btcWalletService = btcWalletService;
|
|
||||||
this.btcFormatter = btcFormatter;
|
|
||||||
this.walletPasswordWindow = walletPasswordWindow;
|
|
||||||
this.openOfferManager = openOfferManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void show() {
|
|
||||||
createGridPane();
|
|
||||||
addHeadLine();
|
|
||||||
addContent();
|
|
||||||
applyStyles();
|
|
||||||
display();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setupKeyHandler(Scene scene) {
|
|
||||||
if (!hideCloseButton) {
|
|
||||||
scene.setOnKeyPressed(e -> {
|
|
||||||
if (e.getCode() == KeyCode.ESCAPE) {
|
|
||||||
e.consume();
|
|
||||||
doClose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addContent() {
|
|
||||||
addMultilineLabel(gridPane, ++rowIndex, Res.get("emptyWalletWindow.info"), 0);
|
|
||||||
|
|
||||||
Coin totalBalance = btcWalletService.getAvailableConfirmedBalance();
|
|
||||||
balanceTextField = addTopLabelTextField(gridPane, ++rowIndex, Res.get("emptyWalletWindow.balance"),
|
|
||||||
btcFormatter.formatCoinWithCode(totalBalance), 10).second;
|
|
||||||
|
|
||||||
addressInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("emptyWalletWindow.address"));
|
|
||||||
|
|
||||||
closeButton = new AutoTooltipButton(Res.get("shared.cancel"));
|
|
||||||
closeButton.setOnAction(e -> {
|
|
||||||
hide();
|
|
||||||
closeHandlerOptional.ifPresent(Runnable::run);
|
|
||||||
});
|
|
||||||
|
|
||||||
emptyWalletButton = new AutoTooltipButton(Res.get("emptyWalletWindow.button"));
|
|
||||||
boolean isBalanceSufficient = Restrictions.isAboveDust(totalBalance);
|
|
||||||
emptyWalletButton.setDefaultButton(isBalanceSufficient);
|
|
||||||
emptyWalletButton.setDisable(!isBalanceSufficient && addressInputTextField.getText().length() > 0);
|
|
||||||
emptyWalletButton.setOnAction(e -> {
|
|
||||||
if (addressInputTextField.getText().length() > 0 && isBalanceSufficient) {
|
|
||||||
log.warn(getClass().getSimpleName() + ".addContent() needs updated for XMR");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
closeButton.setDefaultButton(!isBalanceSufficient);
|
|
||||||
|
|
||||||
HBox hBox = new HBox();
|
|
||||||
hBox.setSpacing(10);
|
|
||||||
GridPane.setRowIndex(hBox, ++rowIndex);
|
|
||||||
hBox.getChildren().addAll(emptyWalletButton, closeButton);
|
|
||||||
gridPane.getChildren().add(hBox);
|
|
||||||
GridPane.setMargin(hBox, new Insets(10, 0, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doEmptyWallet(KeyParameter aesKey) {
|
|
||||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) {
|
|
||||||
if (!openOfferManager.getObservableList().isEmpty()) {
|
|
||||||
UserThread.runAfter(() ->
|
|
||||||
new Popup().warning(Res.get("emptyWalletWindow.openOffers.warn"))
|
|
||||||
.actionButtonText(Res.get("emptyWalletWindow.openOffers.yes"))
|
|
||||||
.onAction(() -> doEmptyWallet2(aesKey))
|
|
||||||
.show(), 300, TimeUnit.MILLISECONDS);
|
|
||||||
} else {
|
|
||||||
doEmptyWallet2(aesKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doEmptyWallet2(KeyParameter aesKey) {
|
|
||||||
emptyWalletButton.setDisable(true);
|
|
||||||
openOfferManager.removeAllOpenOffers(() -> {
|
|
||||||
try {
|
|
||||||
btcWalletService.emptyBtcWallet(addressInputTextField.getText(),
|
|
||||||
aesKey,
|
|
||||||
() -> {
|
|
||||||
closeButton.updateText(Res.get("shared.close"));
|
|
||||||
balanceTextField.setText(btcFormatter.formatCoinWithCode(btcWalletService.getAvailableConfirmedBalance()));
|
|
||||||
emptyWalletButton.setDisable(true);
|
|
||||||
log.debug("wallet empty successful");
|
|
||||||
onClose(() -> UserThread.runAfter(() -> new Popup()
|
|
||||||
.feedback(Res.get("emptyWalletWindow.sent.success"))
|
|
||||||
.show(), Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS));
|
|
||||||
doClose();
|
|
||||||
},
|
|
||||||
(errorMessage) -> {
|
|
||||||
emptyWalletButton.setDisable(false);
|
|
||||||
log.error("wallet empty failed {}", errorMessage);
|
|
||||||
});
|
|
||||||
} catch (InsufficientMoneyException | AddressFormatException e1) {
|
|
||||||
e1.printStackTrace();
|
|
||||||
log.error(e1.getMessage());
|
|
||||||
emptyWalletButton.setDisable(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,821 +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.desktop.main.overlays.windows;
|
|
||||||
|
|
||||||
import de.jensd.fx.fontawesome.AwesomeDude;
|
|
||||||
import de.jensd.fx.fontawesome.AwesomeIcon;
|
|
||||||
import haveno.common.UserThread;
|
|
||||||
import haveno.common.config.Config;
|
|
||||||
import haveno.common.util.Base64;
|
|
||||||
import haveno.common.util.Tuple2;
|
|
||||||
import haveno.common.util.Utilities;
|
|
||||||
import haveno.core.api.CoreMoneroConnectionsService;
|
|
||||||
import haveno.core.locale.Res;
|
|
||||||
import haveno.core.payment.validation.LengthValidator;
|
|
||||||
import haveno.core.payment.validation.PercentageNumberValidator;
|
|
||||||
import haveno.core.support.dispute.Dispute;
|
|
||||||
import haveno.core.support.dispute.mediation.MediationManager;
|
|
||||||
import haveno.core.user.BlockChainExplorer;
|
|
||||||
import haveno.core.user.Preferences;
|
|
||||||
import haveno.core.xmr.exceptions.TransactionVerificationException;
|
|
||||||
import haveno.core.xmr.exceptions.TxBroadcastException;
|
|
||||||
import haveno.core.xmr.exceptions.WalletException;
|
|
||||||
import haveno.core.xmr.wallet.TradeWalletService;
|
|
||||||
import haveno.core.xmr.wallet.TxBroadcaster;
|
|
||||||
import haveno.core.xmr.wallet.WalletsManager;
|
|
||||||
import haveno.desktop.components.AutoTooltipButton;
|
|
||||||
import haveno.desktop.components.HavenoTextArea;
|
|
||||||
import haveno.desktop.components.InputTextField;
|
|
||||||
import haveno.desktop.main.overlays.Overlay;
|
|
||||||
import haveno.desktop.main.overlays.popups.Popup;
|
|
||||||
import haveno.desktop.util.GUIUtil;
|
|
||||||
import haveno.network.p2p.P2PService;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
|
||||||
import javafx.collections.FXCollections;
|
|
||||||
import javafx.collections.ObservableList;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Orientation;
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.Button;
|
|
||||||
import javafx.scene.control.CheckBox;
|
|
||||||
import javafx.scene.control.ComboBox;
|
|
||||||
import javafx.scene.control.Label;
|
|
||||||
import javafx.scene.control.Separator;
|
|
||||||
import javafx.scene.control.TextArea;
|
|
||||||
import javafx.scene.control.Tooltip;
|
|
||||||
import javafx.scene.input.KeyCode;
|
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import org.bitcoinj.core.Address;
|
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
import org.bitcoinj.core.ECKey;
|
|
||||||
import org.bitcoinj.core.SignatureDecodeException;
|
|
||||||
import org.bitcoinj.core.Transaction;
|
|
||||||
import org.bitcoinj.core.Utils;
|
|
||||||
import org.bitcoinj.core.VerificationException;
|
|
||||||
import org.bitcoinj.script.Script;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
import java.security.SignatureException;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static haveno.desktop.util.FormBuilder.addCheckBox;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addInputTextField;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addTopLabelComboBox;
|
|
||||||
|
|
||||||
// We don't translate here as it is for dev only purpose
|
|
||||||
public class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
|
|
||||||
private static final int HEX_HASH_LENGTH = 32 * 2;
|
|
||||||
private static final int HEX_PUBKEY_LENGTH = 33 * 2;
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ManualPayoutTxWindow.class);
|
|
||||||
private final TradeWalletService tradeWalletService;
|
|
||||||
private final P2PService p2PService;
|
|
||||||
private final MediationManager mediationManager;
|
|
||||||
private final Preferences preferences;
|
|
||||||
private final CoreMoneroConnectionsService connectionService;
|
|
||||||
private final WalletsManager walletsManager;
|
|
||||||
GridPane inputsGridPane;
|
|
||||||
GridPane importTxGridPane;
|
|
||||||
GridPane exportTxGridPane;
|
|
||||||
GridPane signTxGridPane;
|
|
||||||
GridPane buildTxGridPane;
|
|
||||||
GridPane signVerifyMsgGridPane;
|
|
||||||
CheckBox depositTxLegacy, recentTickets;
|
|
||||||
ComboBox<String> mediationDropDown;
|
|
||||||
ObservableList<Dispute> disputeObservableList;
|
|
||||||
InputTextField depositTxHex;
|
|
||||||
InputTextField amountInMultisig;
|
|
||||||
InputTextField buyerPayoutAmount;
|
|
||||||
InputTextField sellerPayoutAmount;
|
|
||||||
InputTextField txFee;
|
|
||||||
InputTextField txFeePct;
|
|
||||||
InputTextField buyerAddressString;
|
|
||||||
InputTextField sellerAddressString;
|
|
||||||
InputTextField buyerPubKeyAsHex;
|
|
||||||
InputTextField sellerPubKeyAsHex;
|
|
||||||
InputTextField buyerSignatureAsHex;
|
|
||||||
InputTextField sellerSignatureAsHex;
|
|
||||||
InputTextField privateKeyHex;
|
|
||||||
InputTextField signatureHex;
|
|
||||||
TextArea importHex;
|
|
||||||
TextArea exportHex;
|
|
||||||
TextArea finalSignedTxHex;
|
|
||||||
private ChangeListener<Boolean> txFeeListener, amountInMultisigListener, buyerPayoutAmountListener, sellerPayoutAmountListener;
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Public API
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public ManualPayoutTxWindow(TradeWalletService tradeWalletService,
|
|
||||||
P2PService p2PService,
|
|
||||||
MediationManager mediationManager,
|
|
||||||
Preferences preferences,
|
|
||||||
CoreMoneroConnectionsService connectionService,
|
|
||||||
WalletsManager walletsManager) {
|
|
||||||
this.tradeWalletService = tradeWalletService;
|
|
||||||
this.p2PService = p2PService;
|
|
||||||
this.mediationManager = mediationManager;
|
|
||||||
this.preferences = preferences;
|
|
||||||
this.connectionService = connectionService;
|
|
||||||
this.walletsManager = walletsManager;
|
|
||||||
type = Type.Attention;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void show() {
|
|
||||||
if (headLine == null)
|
|
||||||
headLine = "Emergency MultiSig payout tool"; // We dont translate here as it is for dev only purpose
|
|
||||||
|
|
||||||
width = 1068;
|
|
||||||
createGridPane();
|
|
||||||
addHeadLine();
|
|
||||||
addContent();
|
|
||||||
addButtons();
|
|
||||||
applyStyles();
|
|
||||||
txFeeListener = (observable, oldValue, newValue) -> {
|
|
||||||
calculateTxFee();
|
|
||||||
};
|
|
||||||
buyerPayoutAmountListener = (observable, oldValue, newValue) -> {
|
|
||||||
calculateTxFee();
|
|
||||||
};
|
|
||||||
sellerPayoutAmountListener = (observable, oldValue, newValue) -> {
|
|
||||||
calculateTxFee();
|
|
||||||
};
|
|
||||||
amountInMultisigListener = (observable, oldValue, newValue) -> {
|
|
||||||
calculateTxFee();
|
|
||||||
};
|
|
||||||
txFee.focusedProperty().addListener(txFeeListener);
|
|
||||||
buyerPayoutAmount.focusedProperty().addListener(buyerPayoutAmountListener);
|
|
||||||
sellerPayoutAmount.focusedProperty().addListener(sellerPayoutAmountListener);
|
|
||||||
amountInMultisig.focusedProperty().addListener(amountInMultisigListener);
|
|
||||||
display();
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Protected
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void setupKeyHandler(Scene scene) {
|
|
||||||
if (!hideCloseButton) {
|
|
||||||
scene.setOnKeyPressed(e -> {
|
|
||||||
if (e.getCode() == KeyCode.ESCAPE) {
|
|
||||||
e.consume();
|
|
||||||
doClose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void createGridPane() {
|
|
||||||
gridPane = new GridPane();
|
|
||||||
gridPane.setHgap(15);
|
|
||||||
gridPane.setVgap(15);
|
|
||||||
gridPane.setPadding(new Insets(64, 64, 64, 64));
|
|
||||||
gridPane.setPrefWidth(width);
|
|
||||||
ColumnConstraints columnConstraints1 = new ColumnConstraints();
|
|
||||||
ColumnConstraints columnConstraints2 = new ColumnConstraints();
|
|
||||||
columnConstraints1.setPercentWidth(25);
|
|
||||||
columnConstraints2.setPercentWidth(75);
|
|
||||||
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void cleanup() {
|
|
||||||
txFee.focusedProperty().removeListener(txFeeListener);
|
|
||||||
buyerPayoutAmount.focusedProperty().removeListener(buyerPayoutAmountListener);
|
|
||||||
sellerPayoutAmount.focusedProperty().removeListener(sellerPayoutAmountListener);
|
|
||||||
amountInMultisig.focusedProperty().removeListener(amountInMultisigListener);
|
|
||||||
super.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addContent() {
|
|
||||||
rowIndex = 1;
|
|
||||||
this.disableActionButton = true;
|
|
||||||
addLeftPanelButtons();
|
|
||||||
addInputsPane();
|
|
||||||
addImportPane();
|
|
||||||
addExportPane();
|
|
||||||
addSignPane();
|
|
||||||
addBuildPane();
|
|
||||||
signVerifyMsgGridPane = addSignVerifyMsgPane(new GridPane());
|
|
||||||
hideAllPanes();
|
|
||||||
inputsGridPane.setVisible(true);
|
|
||||||
|
|
||||||
// Notes:
|
|
||||||
// Open with alt+g
|
|
||||||
// Priv key is only visible if pw protection is removed (wallet details data (alt+j))
|
|
||||||
// Take missing buyerPubKeyAsHex and sellerPubKeyAsHex from contract data!
|
|
||||||
// Lookup sellerPrivateKeyAsHex associated with sellerPubKeyAsHex (or buyers) in wallet details data
|
|
||||||
// sellerPubKeys/buyerPubKeys are auto generated if used the fields below
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addLeftPanelButtons() {
|
|
||||||
Button buttonInputs = new AutoTooltipButton("Inputs");
|
|
||||||
Button buttonImport = new AutoTooltipButton("Import");
|
|
||||||
Button buttonExport = new AutoTooltipButton("Export");
|
|
||||||
Button buttonSign = new AutoTooltipButton("Sign");
|
|
||||||
Button buttonBuild = new AutoTooltipButton("Build");
|
|
||||||
Button buttonSignVerifyMsg = new AutoTooltipButton("Sign/Verify Msg");
|
|
||||||
VBox vBox = new VBox(12, buttonInputs, buttonImport, buttonExport, buttonSign, buttonBuild, buttonSignVerifyMsg);
|
|
||||||
vBox.getChildren().forEach(button -> ((Button) button).setPrefWidth(500));
|
|
||||||
gridPane.add(vBox, 0, rowIndex);
|
|
||||||
buttonInputs.getStyleClass().add("action-button");
|
|
||||||
buttonInputs.setOnAction(e -> { // just show the inputs pane
|
|
||||||
hideAllPanes();
|
|
||||||
vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button"));
|
|
||||||
buttonInputs.getStyleClass().add("action-button");
|
|
||||||
inputsGridPane.setVisible(true);
|
|
||||||
});
|
|
||||||
buttonImport.setOnAction(e -> { // just show the import pane
|
|
||||||
hideAllPanes();
|
|
||||||
vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button"));
|
|
||||||
buttonImport.getStyleClass().add("action-button");
|
|
||||||
importTxGridPane.setVisible(true);
|
|
||||||
importHex.setText("");
|
|
||||||
});
|
|
||||||
buttonExport.setOnAction(e -> { // show export pane and fill in the data
|
|
||||||
hideAllPanes();
|
|
||||||
vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button"));
|
|
||||||
buttonExport.getStyleClass().add("action-button");
|
|
||||||
exportTxGridPane.setVisible(true);
|
|
||||||
exportHex.setText(generateExportText());
|
|
||||||
});
|
|
||||||
buttonSign.setOnAction(e -> { // just show the sign pane
|
|
||||||
hideAllPanes();
|
|
||||||
vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button"));
|
|
||||||
buttonSign.getStyleClass().add("action-button");
|
|
||||||
signTxGridPane.setVisible(true);
|
|
||||||
privateKeyHex.setText("");
|
|
||||||
signatureHex.setText("");
|
|
||||||
});
|
|
||||||
buttonBuild.setOnAction(e -> { // just show the build pane
|
|
||||||
hideAllPanes();
|
|
||||||
vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button"));
|
|
||||||
buttonBuild.getStyleClass().add("action-button");
|
|
||||||
buildTxGridPane.setVisible(true);
|
|
||||||
finalSignedTxHex.setText("");
|
|
||||||
});
|
|
||||||
buttonSignVerifyMsg.setOnAction(e -> { // just show the sign msg pane
|
|
||||||
hideAllPanes();
|
|
||||||
vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button"));
|
|
||||||
buttonSignVerifyMsg.getStyleClass().add("action-button");
|
|
||||||
signVerifyMsgGridPane.setVisible(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addInputsPane() {
|
|
||||||
inputsGridPane = new GridPane();
|
|
||||||
gridPane.add(inputsGridPane, 1, rowIndex);
|
|
||||||
int rowIndexA = 0;
|
|
||||||
|
|
||||||
depositTxLegacy = addCheckBox(inputsGridPane, rowIndexA, "depositTxLegacy");
|
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip"));
|
|
||||||
Label blockExplorerIcon = new Label();
|
|
||||||
blockExplorerIcon.getStyleClass().addAll("icon", "highlight");
|
|
||||||
blockExplorerIcon.setTooltip(tooltip);
|
|
||||||
AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK);
|
|
||||||
blockExplorerIcon.setMinWidth(20);
|
|
||||||
blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(depositTxHex.getText()));
|
|
||||||
depositTxHex = addInputTextField(inputsGridPane, rowIndexA, "depositTxId");
|
|
||||||
HBox hBoxTx = new HBox(12, depositTxHex, blockExplorerIcon);
|
|
||||||
hBoxTx.setAlignment(Pos.BASELINE_LEFT);
|
|
||||||
hBoxTx.setPrefWidth(800);
|
|
||||||
inputsGridPane.add(new Label(""), 0, ++rowIndexA); // spacer
|
|
||||||
inputsGridPane.add(hBoxTx, 0, ++rowIndexA);
|
|
||||||
|
|
||||||
amountInMultisig = addInputTextField(inputsGridPane, ++rowIndexA, "amountInMultisig");
|
|
||||||
inputsGridPane.add(new Label(""), 0, ++rowIndexA); // spacer
|
|
||||||
buyerPayoutAmount = addInputTextField(inputsGridPane, rowIndexA, "buyerPayoutAmount");
|
|
||||||
sellerPayoutAmount = addInputTextField(inputsGridPane, rowIndexA, "sellerPayoutAmount");
|
|
||||||
txFee = addInputTextField(inputsGridPane, rowIndexA, "Tx fee");
|
|
||||||
txFee.setEditable(false);
|
|
||||||
txFeePct = addInputTextField(inputsGridPane, rowIndexA, "Tx fee %");
|
|
||||||
txFeePct.setEditable(false);
|
|
||||||
PercentageNumberValidator validator = new PercentageNumberValidator();
|
|
||||||
validator.setMaxValue(10D);
|
|
||||||
txFeePct.setValidator(validator);
|
|
||||||
|
|
||||||
HBox hBox = new HBox(12, buyerPayoutAmount, sellerPayoutAmount, txFee, txFeePct);
|
|
||||||
hBox.setAlignment(Pos.BASELINE_LEFT);
|
|
||||||
hBox.setPrefWidth(800);
|
|
||||||
inputsGridPane.add(hBox, 0, ++rowIndexA);
|
|
||||||
buyerAddressString = addInputTextField(inputsGridPane, ++rowIndexA, "buyerPayoutAddress");
|
|
||||||
sellerAddressString = addInputTextField(inputsGridPane, ++rowIndexA, "sellerPayoutAddress");
|
|
||||||
buyerPubKeyAsHex = addInputTextField(inputsGridPane, ++rowIndexA, "buyerPubKeyAsHex");
|
|
||||||
sellerPubKeyAsHex = addInputTextField(inputsGridPane, ++rowIndexA, "sellerPubKeyAsHex");
|
|
||||||
depositTxHex.setPrefWidth(800);
|
|
||||||
depositTxLegacy.setAllowIndeterminate(false);
|
|
||||||
depositTxLegacy.setSelected(false);
|
|
||||||
depositTxHex.setValidator(new LengthValidator(HEX_HASH_LENGTH, HEX_HASH_LENGTH));
|
|
||||||
buyerAddressString.setValidator(new LengthValidator(20, 80));
|
|
||||||
sellerAddressString.setValidator(new LengthValidator(20, 80));
|
|
||||||
buyerPubKeyAsHex.setValidator(new LengthValidator(HEX_PUBKEY_LENGTH, HEX_PUBKEY_LENGTH));
|
|
||||||
sellerPubKeyAsHex.setValidator(new LengthValidator(HEX_PUBKEY_LENGTH, HEX_PUBKEY_LENGTH));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addImportPane() {
|
|
||||||
int rowIndexB = 0;
|
|
||||||
importTxGridPane = new GridPane();
|
|
||||||
gridPane.add(importTxGridPane, 1, rowIndex);
|
|
||||||
importHex = new HavenoTextArea();
|
|
||||||
importHex.setEditable(true);
|
|
||||||
importHex.setWrapText(true);
|
|
||||||
importHex.setPrefSize(800, 150);
|
|
||||||
importTxGridPane.add(importHex, 0, ++rowIndexB);
|
|
||||||
importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
Button buttonImport = new AutoTooltipButton("Import From String");
|
|
||||||
buttonImport.setOnAction(e -> {
|
|
||||||
// here we need to populate the "inputs" fields from the data contained in the TextArea
|
|
||||||
if (doImport(importHex.getText())) {
|
|
||||||
// switch back to the inputs pane
|
|
||||||
hideAllPanes();
|
|
||||||
inputsGridPane.setVisible(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
HBox hBox = new HBox(12, buttonImport);
|
|
||||||
hBox.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
hBox.setPrefWidth(800);
|
|
||||||
importTxGridPane.add(hBox, 0, ++rowIndexB);
|
|
||||||
importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
|
|
||||||
final Separator separator = new Separator(Orientation.HORIZONTAL);
|
|
||||||
separator.setPadding(new Insets(10, 10, 10, 10));
|
|
||||||
importTxGridPane.add(separator, 0, ++rowIndexB);
|
|
||||||
|
|
||||||
importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
final Tuple2<Label, ComboBox<String>> xTuple = addTopLabelComboBox(importTxGridPane, rowIndexB, "Mediation Ticket", "", 0);
|
|
||||||
mediationDropDown = xTuple.second;
|
|
||||||
recentTickets = addCheckBox(importTxGridPane, rowIndexB, "Recent Tickets");
|
|
||||||
recentTickets.setSelected(true);
|
|
||||||
HBox hBox2 = new HBox(12, mediationDropDown, recentTickets);
|
|
||||||
hBox2.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
hBox2.setPrefWidth(800);
|
|
||||||
importTxGridPane.add(hBox2, 0, ++rowIndexB);
|
|
||||||
populateMediationTicketCombo(recentTickets.isSelected());
|
|
||||||
recentTickets.setOnAction(e -> {
|
|
||||||
populateMediationTicketCombo(recentTickets.isSelected());
|
|
||||||
});
|
|
||||||
importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
Button buttonImportTicket = new AutoTooltipButton("Import From Mediation Ticket");
|
|
||||||
buttonImportTicket.setOnAction(e -> {
|
|
||||||
// here we need to populate the "inputs" fields from the chosen mediator ticket
|
|
||||||
importFromMediationTicket(mediationDropDown.getValue());
|
|
||||||
});
|
|
||||||
HBox hBox3 = new HBox(12, buttonImportTicket);
|
|
||||||
hBox3.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
hBox3.setPrefWidth(800);
|
|
||||||
importTxGridPane.add(hBox3, 0, ++rowIndexB);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addExportPane() {
|
|
||||||
exportTxGridPane = new GridPane();
|
|
||||||
gridPane.add(exportTxGridPane, 1, rowIndex);
|
|
||||||
exportHex = new HavenoTextArea();
|
|
||||||
exportHex.setEditable(false);
|
|
||||||
exportHex.setWrapText(true);
|
|
||||||
exportHex.setPrefSize(800, 250);
|
|
||||||
exportTxGridPane.add(exportHex, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addSignPane() {
|
|
||||||
int rowIndexB = 0;
|
|
||||||
signTxGridPane = new GridPane();
|
|
||||||
gridPane.add(signTxGridPane, 1, rowIndex);
|
|
||||||
privateKeyHex = addInputTextField(inputsGridPane, ++rowIndexB, "privateKeyHex");
|
|
||||||
signTxGridPane.add(privateKeyHex, 0, ++rowIndexB);
|
|
||||||
signatureHex = addInputTextField(signTxGridPane, ++rowIndexB, "signatureHex");
|
|
||||||
signatureHex.setPrefWidth(800);
|
|
||||||
signatureHex.setEditable(false);
|
|
||||||
Label copyIcon = new Label();
|
|
||||||
copyIcon.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip")));
|
|
||||||
AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY);
|
|
||||||
copyIcon.getStyleClass().addAll("icon", "highlight");
|
|
||||||
copyIcon.setMinWidth(20);
|
|
||||||
copyIcon.setOnMouseClicked(mouseEvent -> Utilities.copyToClipboard(signatureHex.getText()));
|
|
||||||
HBox hBoxSig = new HBox(12, signatureHex, copyIcon);
|
|
||||||
hBoxSig.setAlignment(Pos.BASELINE_LEFT);
|
|
||||||
hBoxSig.setPrefWidth(800);
|
|
||||||
signTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
signTxGridPane.add(hBoxSig, 0, ++rowIndexB);
|
|
||||||
signTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
Button buttonLocate = new AutoTooltipButton("Locate key in wallet");
|
|
||||||
Button buttonSign = new AutoTooltipButton("Generate Signature");
|
|
||||||
HBox hBox = new HBox(12, buttonLocate, buttonSign);
|
|
||||||
hBox.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
hBox.setPrefWidth(800);
|
|
||||||
signTxGridPane.add(hBox, 0, ++rowIndexB);
|
|
||||||
buttonLocate.setOnAction(e -> {
|
|
||||||
if (!validateInputFields()) {
|
|
||||||
signatureHex.setText("You need to fill in the inputs tab first");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String walletInfo = walletsManager.getWalletsAsString(true);
|
|
||||||
String privateKeyText = findPrivForPubOrAddress(walletInfo, buyerPubKeyAsHex.getText());
|
|
||||||
if (privateKeyText == null) {
|
|
||||||
privateKeyText = findPrivForPubOrAddress(walletInfo, sellerPubKeyAsHex.getText());
|
|
||||||
}
|
|
||||||
if (privateKeyText == null) {
|
|
||||||
privateKeyText = "Not found in wallet";
|
|
||||||
}
|
|
||||||
privateKeyHex.setText(privateKeyText);
|
|
||||||
});
|
|
||||||
buttonSign.setOnAction(e -> {
|
|
||||||
signatureHex.setText(generateSignature());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addBuildPane() {
|
|
||||||
buildTxGridPane = new GridPane();
|
|
||||||
gridPane.add(buildTxGridPane, 1, rowIndex);
|
|
||||||
int rowIndexA = 0;
|
|
||||||
buyerSignatureAsHex = addInputTextField(buildTxGridPane, ++rowIndexA, "buyerSignatureAsHex");
|
|
||||||
sellerSignatureAsHex = addInputTextField(buildTxGridPane, ++rowIndexA, "sellerSignatureAsHex");
|
|
||||||
buildTxGridPane.add(new Label(""), 0, ++rowIndexA); // spacer
|
|
||||||
finalSignedTxHex = new HavenoTextArea();
|
|
||||||
finalSignedTxHex.setEditable(false);
|
|
||||||
finalSignedTxHex.setWrapText(true);
|
|
||||||
finalSignedTxHex.setPrefSize(800, 250);
|
|
||||||
buildTxGridPane.add(finalSignedTxHex, 0, ++rowIndexA);
|
|
||||||
buildTxGridPane.add(new Label(""), 0, ++rowIndexA); // spacer
|
|
||||||
Button buttonBuild = new AutoTooltipButton("Build");
|
|
||||||
Button buttonBroadcast = new AutoTooltipButton("Broadcast");
|
|
||||||
HBox hBox = new HBox(12, buttonBuild, buttonBroadcast);
|
|
||||||
hBox.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
hBox.setPrefWidth(800);
|
|
||||||
buildTxGridPane.add(hBox, 0, ++rowIndexA);
|
|
||||||
buttonBuild.setOnAction(e -> {
|
|
||||||
finalSignedTxHex.setText(buildFinalTx(false));
|
|
||||||
});
|
|
||||||
buttonBroadcast.setOnAction(e -> {
|
|
||||||
finalSignedTxHex.setText(buildFinalTx(true));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private GridPane addSignVerifyMsgPane(GridPane myGridPane) {
|
|
||||||
int rowIndexB = 0;
|
|
||||||
gridPane.add(myGridPane, 1, rowIndex);
|
|
||||||
TextArea messageText = new HavenoTextArea();
|
|
||||||
messageText.setPromptText("Message");
|
|
||||||
messageText.setEditable(true);
|
|
||||||
messageText.setWrapText(true);
|
|
||||||
messageText.setPrefSize(800, 150);
|
|
||||||
myGridPane.add(messageText, 0, ++rowIndexB);
|
|
||||||
myGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
InputTextField address = addInputTextField(myGridPane, ++rowIndexB, "Address");
|
|
||||||
myGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
TextArea messageSig = new HavenoTextArea();
|
|
||||||
messageSig.setPromptText("Signature");
|
|
||||||
messageSig.setEditable(true);
|
|
||||||
messageSig.setWrapText(true);
|
|
||||||
messageSig.setPrefSize(800, 65);
|
|
||||||
myGridPane.add(messageSig, 0, ++rowIndexB);
|
|
||||||
myGridPane.add(new Label(""), 0, ++rowIndexB); // spacer
|
|
||||||
Button buttonSign = new AutoTooltipButton("Sign");
|
|
||||||
Button buttonVerify = new AutoTooltipButton("Verify");
|
|
||||||
HBox buttonBox = new HBox(12, buttonSign, buttonVerify);
|
|
||||||
buttonBox.setAlignment(Pos.BASELINE_CENTER);
|
|
||||||
buttonBox.setPrefWidth(800);
|
|
||||||
myGridPane.add(buttonBox, 0, ++rowIndexB);
|
|
||||||
|
|
||||||
buttonSign.setOnAction(e -> {
|
|
||||||
String walletInfo = walletsManager.getWalletsAsString(true);
|
|
||||||
String privKeyHex = findPrivForPubOrAddress(walletInfo, address.getText());
|
|
||||||
if (privKeyHex == null) {
|
|
||||||
messageSig.setText("");
|
|
||||||
new Popup().information("Key not found in wallet").show();
|
|
||||||
} else {
|
|
||||||
ECKey myPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(privKeyHex));
|
|
||||||
String signatureBase64 = myPrivateKey.signMessage(messageText.getText());
|
|
||||||
messageSig.setText(signatureBase64);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
buttonVerify.setOnAction(e -> {
|
|
||||||
try {
|
|
||||||
ECKey key = ECKey.signedMessageToKey(messageText.getText(), messageSig.getText());
|
|
||||||
Address address1 = Address.fromKey(Config.baseCurrencyNetworkParameters(), key, Script.ScriptType.P2PKH);
|
|
||||||
Address address2 = Address.fromKey(Config.baseCurrencyNetworkParameters(), key, Script.ScriptType.P2WPKH);
|
|
||||||
if (address.getText().equalsIgnoreCase(address1.toString()) ||
|
|
||||||
address.getText().equalsIgnoreCase(address2.toString())) {
|
|
||||||
new Popup().information("Signature verified").show();
|
|
||||||
} else {
|
|
||||||
new Popup().warning("Wrong signature").show();
|
|
||||||
}
|
|
||||||
} catch (SignatureException ex) {
|
|
||||||
log.warn(ex.toString());
|
|
||||||
new Popup().warning("Wrong signature").show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return myGridPane;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideAllPanes() {
|
|
||||||
inputsGridPane.setVisible(false);
|
|
||||||
importTxGridPane.setVisible(false);
|
|
||||||
exportTxGridPane.setVisible(false);
|
|
||||||
signTxGridPane.setVisible(false);
|
|
||||||
buildTxGridPane.setVisible(false);
|
|
||||||
signVerifyMsgGridPane.setVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void populateMediationTicketCombo(boolean recentTicketsOnly) {
|
|
||||||
Instant twoWeeksAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(14));
|
|
||||||
disputeObservableList = mediationManager.getDisputesAsObservableList();
|
|
||||||
ObservableList<String> disputeIds = FXCollections.observableArrayList();
|
|
||||||
for (Dispute dispute :disputeObservableList) {
|
|
||||||
if (dispute.getDisputePayoutTxId() != null) // only show disputes not paid out
|
|
||||||
continue;
|
|
||||||
if (recentTicketsOnly && dispute.getOpeningDate().toInstant().isBefore(twoWeeksAgo))
|
|
||||||
continue;
|
|
||||||
if (!disputeIds.contains(dispute.getTradeId()))
|
|
||||||
disputeIds.add(dispute.getTradeId());
|
|
||||||
}
|
|
||||||
disputeIds.sort((a, b) -> a.compareTo(b));
|
|
||||||
mediationDropDown.setItems(disputeIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearInputFields() {
|
|
||||||
depositTxHex.setText("");
|
|
||||||
amountInMultisig.setText("");
|
|
||||||
buyerPayoutAmount.setText("");
|
|
||||||
sellerPayoutAmount.setText("");
|
|
||||||
buyerAddressString.setText("");
|
|
||||||
sellerAddressString.setText("");
|
|
||||||
buyerPubKeyAsHex.setText("");
|
|
||||||
sellerPubKeyAsHex.setText("");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean validateInputFields() {
|
|
||||||
return (depositTxHex.getText().length() == HEX_HASH_LENGTH &&
|
|
||||||
amountInMultisig.getText().length() > 0 &&
|
|
||||||
buyerPayoutAmount.getText().length() > 0 &&
|
|
||||||
sellerPayoutAmount.getText().length() > 0 &&
|
|
||||||
txFee.getText().length() > 0 &&
|
|
||||||
buyerAddressString.getText().length() > 0 &&
|
|
||||||
sellerAddressString.getText().length() > 0 &&
|
|
||||||
buyerPubKeyAsHex.getText().length() == HEX_PUBKEY_LENGTH &&
|
|
||||||
sellerPubKeyAsHex.getText().length() == HEX_PUBKEY_LENGTH &&
|
|
||||||
txFeePct.getValidator().validate(txFeePct.getText()).isValid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean validateInputFieldsAndSignatures() {
|
|
||||||
return (validateInputFields() &&
|
|
||||||
buyerSignatureAsHex.getText().length() > 0 &&
|
|
||||||
sellerSignatureAsHex.getText().length() > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Coin getInputFieldAsCoin(InputTextField inputTextField) {
|
|
||||||
try {
|
|
||||||
return Coin.parseCoin(inputTextField.getText().trim());
|
|
||||||
} catch (RuntimeException ignore) {
|
|
||||||
}
|
|
||||||
return Coin.ZERO;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void calculateTxFee() {
|
|
||||||
if (buyerPayoutAmount.getText().length() > 0 &&
|
|
||||||
sellerPayoutAmount.getText().length() > 0 &&
|
|
||||||
amountInMultisig.getText().length() > 0) {
|
|
||||||
Coin txFeeValue = getInputFieldAsCoin(amountInMultisig)
|
|
||||||
.subtract(getInputFieldAsCoin(buyerPayoutAmount))
|
|
||||||
.subtract(getInputFieldAsCoin(sellerPayoutAmount));
|
|
||||||
txFee.setText(txFeeValue.toPlainString());
|
|
||||||
double feePercent = (double) txFeeValue.value / getInputFieldAsCoin(amountInMultisig).value;
|
|
||||||
txFeePct.setText(String.format("%.2f", feePercent * 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openBlockExplorer(String txId) {
|
|
||||||
if (txId.length() != HEX_HASH_LENGTH)
|
|
||||||
return;
|
|
||||||
if (preferences != null) {
|
|
||||||
BlockChainExplorer blockChainExplorer = preferences.getBlockChainExplorer();
|
|
||||||
GUIUtil.openWebPage(blockChainExplorer.txUrl + txId, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String findPrivForPubOrAddress(String walletInfo, String searchKey) {
|
|
||||||
// split the walletInfo into lines, strip whitespace
|
|
||||||
// look for lines beginning " addr:" followed by "DeterministicKey{pub HEX=" .... ", priv HEX="
|
|
||||||
int lineIndex = 0;
|
|
||||||
while (lineIndex < walletInfo.length() && lineIndex != -1) {
|
|
||||||
lineIndex = walletInfo.indexOf(" addr:", lineIndex);
|
|
||||||
if (lineIndex == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int toIndex = walletInfo.indexOf("}", lineIndex);
|
|
||||||
if (toIndex == -1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String candidate1 = walletInfo.substring(lineIndex, toIndex);
|
|
||||||
lineIndex = toIndex;
|
|
||||||
// do we have the search key?
|
|
||||||
if (candidate1.indexOf(searchKey, 0) > -1) {
|
|
||||||
int startOfPriv = candidate1.indexOf("priv HEX=", 0);
|
|
||||||
if (startOfPriv > -1) {
|
|
||||||
return candidate1.substring(startOfPriv + 9, startOfPriv + 9 + HEX_HASH_LENGTH);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateExportText() {
|
|
||||||
// check that all input fields have been entered, except signatures
|
|
||||||
ArrayList<String> fieldList = new ArrayList<>();
|
|
||||||
fieldList.add(depositTxLegacy.isSelected() ? "legacy" : "segwit");
|
|
||||||
fieldList.add(depositTxHex.getText());
|
|
||||||
fieldList.add(amountInMultisig.getText());
|
|
||||||
fieldList.add(buyerPayoutAmount.getText());
|
|
||||||
fieldList.add(sellerPayoutAmount.getText());
|
|
||||||
fieldList.add(buyerAddressString.getText());
|
|
||||||
fieldList.add(sellerAddressString.getText());
|
|
||||||
fieldList.add(buyerPubKeyAsHex.getText());
|
|
||||||
fieldList.add(sellerPubKeyAsHex.getText());
|
|
||||||
for (String item : fieldList) {
|
|
||||||
if (item.length() < 1) {
|
|
||||||
return "You need to fill in the inputs first";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String listString = String.join(":", fieldList);
|
|
||||||
String base64encoded = Base64.encode(listString.getBytes());
|
|
||||||
return base64encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean doImport(String importedText) {
|
|
||||||
try {
|
|
||||||
clearInputFields();
|
|
||||||
String decoded = new String(Base64.decode(importedText.replaceAll("\\s+", "")), Charset.forName("UTF-8"));
|
|
||||||
String splitArray[] = decoded.split(":");
|
|
||||||
if (splitArray.length < 9) {
|
|
||||||
importHex.setText("Import failed - data format incorrect");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int fieldIndex = 0;
|
|
||||||
depositTxLegacy.setSelected(splitArray[fieldIndex++].equalsIgnoreCase("legacy"));
|
|
||||||
depositTxHex.setText(splitArray[fieldIndex++]);
|
|
||||||
amountInMultisig.setText(splitArray[fieldIndex++]);
|
|
||||||
buyerPayoutAmount.setText(splitArray[fieldIndex++]);
|
|
||||||
sellerPayoutAmount.setText(splitArray[fieldIndex++]);
|
|
||||||
buyerAddressString.setText(splitArray[fieldIndex++]);
|
|
||||||
sellerAddressString.setText(splitArray[fieldIndex++]);
|
|
||||||
buyerPubKeyAsHex.setText(splitArray[fieldIndex++]);
|
|
||||||
sellerPubKeyAsHex.setText(splitArray[fieldIndex++]);
|
|
||||||
calculateTxFee();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
importHex.setText("Import failed - base64 string incorrect");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void importFromMediationTicket(String tradeId) {
|
|
||||||
throw new RuntimeException("ManualPayoutTxWindow.importFromMediationTicket() not adapted to XMR");
|
|
||||||
// clearInputFields();
|
|
||||||
// Optional<Dispute> optionalDispute = mediationManager.findDispute(tradeId);
|
|
||||||
// if (optionalDispute.isPresent()) {
|
|
||||||
// Dispute dispute = optionalDispute.get();
|
|
||||||
// depositTxHex.setText(dispute.getDepositTxId());
|
|
||||||
// if (dispute.disputeResultProperty().get() != null) {
|
|
||||||
// buyerPayoutAmount.setText(dispute.disputeResultProperty().get().getBuyerPayoutAmount().toPlainString());
|
|
||||||
// sellerPayoutAmount.setText(dispute.disputeResultProperty().get().getSellerPayoutAmount().toPlainString());
|
|
||||||
// }
|
|
||||||
// buyerAddressString.setText(dispute.getContract().getBuyerPayoutAddressString());
|
|
||||||
// sellerAddressString.setText(dispute.getContract().getSellerPayoutAddressString());
|
|
||||||
// buyerPubKeyAsHex.setText(Utils.HEX.encode(dispute.getContract().getBuyerMultiSigPubKey()));
|
|
||||||
// sellerPubKeyAsHex.setText(Utils.HEX.encode(dispute.getContract().getSellerMultiSigPubKey()));
|
|
||||||
// // switch back to the inputs pane
|
|
||||||
// hideAllPanes();
|
|
||||||
// inputsGridPane.setVisible(true);
|
|
||||||
// UserThread.execute(() -> new Popup().warning("Ticket imported. You still need to enter the multisig amount and specify if it is a legacy Tx").show());
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateSignature() {
|
|
||||||
calculateTxFee();
|
|
||||||
// check that all input fields have been entered, except signatures
|
|
||||||
if (!validateInputFields() || privateKeyHex.getText().length() < 1) {
|
|
||||||
return "You need to fill in the inputs first";
|
|
||||||
}
|
|
||||||
|
|
||||||
String retVal = "";
|
|
||||||
try {
|
|
||||||
Tuple2<String, String> combined = tradeWalletService.emergencyBuildPayoutTxFrom2of2MultiSig(depositTxHex.getText(),
|
|
||||||
getInputFieldAsCoin(buyerPayoutAmount),
|
|
||||||
getInputFieldAsCoin(sellerPayoutAmount),
|
|
||||||
getInputFieldAsCoin(txFee),
|
|
||||||
buyerAddressString.getText(),
|
|
||||||
sellerAddressString.getText(),
|
|
||||||
buyerPubKeyAsHex.getText(),
|
|
||||||
sellerPubKeyAsHex.getText(),
|
|
||||||
depositTxLegacy.isSelected());
|
|
||||||
String redeemScriptHex = combined.first;
|
|
||||||
String unsignedTxHex = combined.second;
|
|
||||||
retVal = tradeWalletService.emergencyGenerateSignature(
|
|
||||||
unsignedTxHex,
|
|
||||||
redeemScriptHex,
|
|
||||||
getInputFieldAsCoin(amountInMultisig),
|
|
||||||
privateKeyHex.getText());
|
|
||||||
} catch (IllegalArgumentException ee) {
|
|
||||||
log.error(ee.toString());
|
|
||||||
ee.printStackTrace();
|
|
||||||
UserThread.execute(() -> new Popup().warning(ee.toString()).show());
|
|
||||||
}
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildFinalTx(boolean broadcastIt) {
|
|
||||||
String retVal = "";
|
|
||||||
calculateTxFee();
|
|
||||||
// check that all input fields have been entered, including signatures
|
|
||||||
if (!validateInputFieldsAndSignatures()) {
|
|
||||||
retVal = "You need to fill in the inputs first";
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
// grab data from the inputs pane, build an unsigned tx and write it to the TextArea
|
|
||||||
Tuple2<String, String> combined = tradeWalletService.emergencyBuildPayoutTxFrom2of2MultiSig(depositTxHex.getText(),
|
|
||||||
getInputFieldAsCoin(buyerPayoutAmount),
|
|
||||||
getInputFieldAsCoin(sellerPayoutAmount),
|
|
||||||
getInputFieldAsCoin(txFee),
|
|
||||||
buyerAddressString.getText(),
|
|
||||||
sellerAddressString.getText(),
|
|
||||||
buyerPubKeyAsHex.getText(),
|
|
||||||
sellerPubKeyAsHex.getText(),
|
|
||||||
depositTxLegacy.isSelected());
|
|
||||||
String redeemScriptHex = combined.first;
|
|
||||||
String unsignedTxHex = combined.second;
|
|
||||||
Tuple2<String, String> txIdAndHex = tradeWalletService.emergencyApplySignatureToPayoutTxFrom2of2MultiSig(
|
|
||||||
unsignedTxHex,
|
|
||||||
redeemScriptHex,
|
|
||||||
buyerSignatureAsHex.getText(),
|
|
||||||
sellerSignatureAsHex.getText(),
|
|
||||||
depositTxLegacy.isSelected());
|
|
||||||
retVal = "txId:{" + txIdAndHex.first + "}\r\ntxHex:{" + txIdAndHex.second + "}";
|
|
||||||
|
|
||||||
if (broadcastIt) {
|
|
||||||
TxBroadcaster.Callback callback = new TxBroadcaster.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(@Nullable Transaction result) {
|
|
||||||
log.info("onSuccess");
|
|
||||||
UserThread.execute(() -> {
|
|
||||||
String txId = result != null ? result.getTxId().toString() : "null";
|
|
||||||
new Popup().information("Transaction successfully published. Transaction ID: " + txId).show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onFailure(TxBroadcastException exception) {
|
|
||||||
log.error(exception.toString());
|
|
||||||
UserThread.execute(() -> new Popup().warning(exception.toString()).show());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, connectionService)) {
|
|
||||||
try {
|
|
||||||
tradeWalletService.emergencyPublishPayoutTxFrom2of2MultiSig(
|
|
||||||
txIdAndHex.second,
|
|
||||||
callback);
|
|
||||||
} catch (AddressFormatException | WalletException | TransactionVerificationException ee) {
|
|
||||||
log.error(ee.toString());
|
|
||||||
ee.printStackTrace();
|
|
||||||
UserThread.execute(() -> new Popup().warning(ee.toString()).show());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException | SignatureDecodeException | VerificationException ee) {
|
|
||||||
log.error(ee.toString());
|
|
||||||
ee.printStackTrace();
|
|
||||||
retVal = ee.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,118 +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.desktop.main.overlays.windows;
|
|
||||||
|
|
||||||
import haveno.core.locale.Res;
|
|
||||||
import haveno.core.trade.txproof.xmr.XmrTxProofModel;
|
|
||||||
import haveno.core.util.validation.RegexValidator;
|
|
||||||
import haveno.desktop.components.InputTextField;
|
|
||||||
import haveno.desktop.main.overlays.Overlay;
|
|
||||||
import javafx.geometry.HPos;
|
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
|
||||||
import javafx.scene.layout.GridPane;
|
|
||||||
import javafx.scene.layout.Priority;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import static haveno.common.app.DevEnv.isDevMode;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addInputTextField;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addMultilineLabel;
|
|
||||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
|
||||||
|
|
||||||
public class SetXmrTxKeyWindow extends Overlay<SetXmrTxKeyWindow> {
|
|
||||||
|
|
||||||
private InputTextField txHashInputTextField, txKeyInputTextField;
|
|
||||||
@Getter
|
|
||||||
private RegexValidator regexValidator;
|
|
||||||
|
|
||||||
public SetXmrTxKeyWindow() {
|
|
||||||
type = Type.Attention;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
if (headLine == null)
|
|
||||||
headLine = Res.get("setXMRTxKeyWindow.headline");
|
|
||||||
|
|
||||||
width = 868;
|
|
||||||
createGridPane();
|
|
||||||
addHeadLine();
|
|
||||||
addContent();
|
|
||||||
addButtons();
|
|
||||||
|
|
||||||
regexValidator = new RegexValidator();
|
|
||||||
regexValidator.setPattern("[a-fA-F0-9]{64}|^$");
|
|
||||||
regexValidator.setErrorMessage(Res.get("portfolio.pending.step2_buyer.confirmStart.proof.invalidInput"));
|
|
||||||
txHashInputTextField.setValidator(regexValidator);
|
|
||||||
txKeyInputTextField.setValidator(regexValidator);
|
|
||||||
if (isDevMode()) {
|
|
||||||
// pre-populate the fields with test data when in dev mode
|
|
||||||
txHashInputTextField.setText(XmrTxProofModel.DEV_TX_HASH);
|
|
||||||
txKeyInputTextField.setText(XmrTxProofModel.DEV_TX_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
actionButton.disableProperty().bind(createBooleanBinding(() -> {
|
|
||||||
String txHash = txHashInputTextField.getText();
|
|
||||||
String txKey = txKeyInputTextField.getText();
|
|
||||||
|
|
||||||
// If a field is empty we allow to continue. We do not enforce that users send the data.
|
|
||||||
if (txHash.isEmpty() || txKey.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise we require that input is valid
|
|
||||||
return !txHashInputTextField.getValidator().validate(txHash).isValid ||
|
|
||||||
!txKeyInputTextField.getValidator().validate(txKey).isValid;
|
|
||||||
},
|
|
||||||
txHashInputTextField.textProperty(), txKeyInputTextField.textProperty()));
|
|
||||||
|
|
||||||
applyStyles();
|
|
||||||
display();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void createGridPane() {
|
|
||||||
gridPane = new GridPane();
|
|
||||||
gridPane.setHgap(5);
|
|
||||||
gridPane.setVgap(5);
|
|
||||||
gridPane.setPadding(new Insets(64, 64, 64, 64));
|
|
||||||
gridPane.setPrefWidth(width);
|
|
||||||
|
|
||||||
ColumnConstraints columnConstraints1 = new ColumnConstraints();
|
|
||||||
columnConstraints1.setHalignment(HPos.RIGHT);
|
|
||||||
columnConstraints1.setHgrow(Priority.SOMETIMES);
|
|
||||||
gridPane.getColumnConstraints().addAll(columnConstraints1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getTxHash() {
|
|
||||||
return txHashInputTextField != null ? txHashInputTextField.getText() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getTxKey() {
|
|
||||||
return txKeyInputTextField != null ? txKeyInputTextField.getText() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addContent() {
|
|
||||||
addMultilineLabel(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.note"), 0);
|
|
||||||
txHashInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.txHash"), 10);
|
|
||||||
txKeyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.txKey"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -29,7 +29,6 @@ import haveno.core.trade.Contract;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.Trade;
|
import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.TradeManager;
|
import haveno.core.trade.TradeManager;
|
||||||
import haveno.core.trade.txproof.AssetTxProofResult;
|
|
||||||
import haveno.core.util.FormattingUtils;
|
import haveno.core.util.FormattingUtils;
|
||||||
import haveno.core.util.VolumeUtil;
|
import haveno.core.util.VolumeUtil;
|
||||||
import haveno.core.util.coin.CoinFormatter;
|
import haveno.core.util.coin.CoinFormatter;
|
||||||
|
@ -38,7 +37,6 @@ import haveno.desktop.components.HavenoTextArea;
|
||||||
import haveno.desktop.main.MainView;
|
import haveno.desktop.main.MainView;
|
||||||
import haveno.desktop.main.overlays.Overlay;
|
import haveno.desktop.main.overlays.Overlay;
|
||||||
import haveno.desktop.util.DisplayUtils;
|
import haveno.desktop.util.DisplayUtils;
|
||||||
import haveno.desktop.util.GUIUtil;
|
|
||||||
import haveno.desktop.util.Layout;
|
import haveno.desktop.util.Layout;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
@ -64,7 +62,6 @@ import org.slf4j.LoggerFactory;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
import static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription;
|
import static haveno.desktop.util.DisplayUtils.getAccountWitnessDescription;
|
||||||
import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox;
|
import static haveno.desktop.util.FormBuilder.add2ButtonsWithBox;
|
||||||
import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea;
|
import static haveno.desktop.util.FormBuilder.addConfirmationLabelTextArea;
|
||||||
|
@ -188,10 +185,6 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
rows++;
|
rows++;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean showXmrProofResult = checkNotNull(trade.getOffer()).getCurrencyCode().equals("XMR") &&
|
|
||||||
trade.getAssetTxProofResult() != null &&
|
|
||||||
trade.getAssetTxProofResult() != AssetTxProofResult.UNDEFINED;
|
|
||||||
|
|
||||||
if (trade.getPayoutTxId() != null)
|
if (trade.getPayoutTxId() != null)
|
||||||
rows++;
|
rows++;
|
||||||
boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() &&
|
boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() &&
|
||||||
|
@ -202,8 +195,6 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
rows += 2;
|
rows += 2;
|
||||||
if (trade.getTradePeerNodeAddress() != null)
|
if (trade.getTradePeerNodeAddress() != null)
|
||||||
rows++;
|
rows++;
|
||||||
if (showXmrProofResult)
|
|
||||||
rows++;
|
|
||||||
|
|
||||||
addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE);
|
addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE);
|
||||||
addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.tradeId"),
|
addConfirmationLabelTextField(gridPane, rowIndex, Res.get("shared.tradeId"),
|
||||||
|
@ -230,14 +221,6 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"),
|
addConfirmationLabelTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradePeersOnion"),
|
||||||
trade.getTradePeerNodeAddress().getFullAddress());
|
trade.getTradePeerNodeAddress().getFullAddress());
|
||||||
|
|
||||||
if (showXmrProofResult) {
|
|
||||||
// As the window is already overloaded we replace the tradePeersPubKeyHash field with the auto-conf state
|
|
||||||
// if XMR is the currency
|
|
||||||
addConfirmationLabelTextField(gridPane, ++rowIndex,
|
|
||||||
Res.get("portfolio.pending.step3_seller.autoConf.status.label"),
|
|
||||||
GUIUtil.getProofResultAsString(trade.getAssetTxProofResult()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contract != null) {
|
if (contract != null) {
|
||||||
buyersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), buyerPaymentAccountPayload, contract.getBuyerPubKeyRing());
|
buyersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), buyerPaymentAccountPayload, contract.getBuyerPubKeyRing());
|
||||||
sellersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), sellerPaymentAccountPayload, contract.getSellerPubKeyRing());
|
sellersAccountAge = getAccountWitnessDescription(accountAgeWitnessService, offer.getPaymentMethod(), sellerPaymentAccountPayload, contract.getSellerPubKeyRing());
|
||||||
|
|
|
@ -96,11 +96,9 @@ import haveno.desktop.components.paymentmethods.VerseForm;
|
||||||
import haveno.desktop.components.paymentmethods.WeChatPayForm;
|
import haveno.desktop.components.paymentmethods.WeChatPayForm;
|
||||||
import haveno.desktop.components.paymentmethods.WesternUnionForm;
|
import haveno.desktop.components.paymentmethods.WesternUnionForm;
|
||||||
import haveno.desktop.main.overlays.popups.Popup;
|
import haveno.desktop.main.overlays.popups.Popup;
|
||||||
import haveno.desktop.main.overlays.windows.SetXmrTxKeyWindow;
|
|
||||||
import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
|
import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
|
||||||
import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
|
import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
|
||||||
import haveno.desktop.util.Layout;
|
import haveno.desktop.util.Layout;
|
||||||
import haveno.desktop.util.Transitions;
|
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
|
@ -110,7 +108,6 @@ import org.fxmisc.easybind.EasyBind;
|
||||||
import org.fxmisc.easybind.Subscription;
|
import org.fxmisc.easybind.Subscription;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabel;
|
import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabel;
|
||||||
|
@ -543,44 +540,11 @@ public class BuyerStep2View extends TradeStepView {
|
||||||
} else {
|
} else {
|
||||||
showConfirmPaymentSentPopup();
|
showConfirmPaymentSentPopup();
|
||||||
}
|
}
|
||||||
} else if (sellersPaymentAccountPayload instanceof AssetAccountPayload && isXmrTrade()) {
|
|
||||||
SetXmrTxKeyWindow setXmrTxKeyWindow = new SetXmrTxKeyWindow();
|
|
||||||
setXmrTxKeyWindow
|
|
||||||
.actionButtonText(Res.get("portfolio.pending.step2_buyer.confirmStart.headline"))
|
|
||||||
.onAction(() -> {
|
|
||||||
String txKey = setXmrTxKeyWindow.getTxKey();
|
|
||||||
String txHash = setXmrTxKeyWindow.getTxHash();
|
|
||||||
if (txKey == null || txHash == null || txKey.isEmpty() || txHash.isEmpty()) {
|
|
||||||
UserThread.runAfter(this::showProofWarningPopup, Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
trade.setCounterCurrencyExtraData(txKey);
|
|
||||||
trade.setCounterCurrencyTxId(txHash);
|
|
||||||
|
|
||||||
model.dataModel.getTradeManager().requestPersistence();
|
|
||||||
showConfirmPaymentSentPopup();
|
|
||||||
})
|
|
||||||
.closeButtonText(Res.get("shared.cancel"))
|
|
||||||
.onClose(setXmrTxKeyWindow::hide)
|
|
||||||
.show();
|
|
||||||
} else {
|
} else {
|
||||||
showConfirmPaymentSentPopup();
|
showConfirmPaymentSentPopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showProofWarningPopup() {
|
|
||||||
Popup popup = new Popup();
|
|
||||||
popup.headLine(Res.get("portfolio.pending.step2_buyer.confirmStart.proof.warningTitle"))
|
|
||||||
.confirmation(Res.get("portfolio.pending.step2_buyer.confirmStart.proof.noneProvided"))
|
|
||||||
.width(700)
|
|
||||||
.actionButtonText(Res.get("portfolio.pending.step2_buyer.confirmStart.warningButton"))
|
|
||||||
.onAction(this::showConfirmPaymentSentPopup)
|
|
||||||
.closeButtonText(Res.get("shared.cancel"))
|
|
||||||
.onClose(popup::hide)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showConfirmPaymentSentPopup() {
|
private void showConfirmPaymentSentPopup() {
|
||||||
String key = "confirmPaymentSent";
|
String key = "confirmPaymentSent";
|
||||||
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
|
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXBadge;
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.app.DevEnv;
|
import haveno.common.app.DevEnv;
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.trade.txproof.AssetTxProofResult;
|
|
||||||
import haveno.core.user.DontShowAgainLookup;
|
import haveno.core.user.DontShowAgainLookup;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.desktop.components.AutoTooltipButton;
|
import haveno.desktop.components.AutoTooltipButton;
|
||||||
|
@ -88,15 +87,11 @@ public class BuyerStep4View extends TradeStepView {
|
||||||
} else {
|
} else {
|
||||||
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle"));
|
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle"));
|
||||||
}
|
}
|
||||||
JFXBadge autoConfBadge = new JFXBadge(new Label(""), Pos.BASELINE_RIGHT);
|
|
||||||
autoConfBadge.setText(Res.get("portfolio.pending.autoConf"));
|
|
||||||
autoConfBadge.getStyleClass().add("auto-conf");
|
|
||||||
|
|
||||||
HBox hBox2 = new HBox(1, completedTradeLabel, autoConfBadge);
|
HBox hBox2 = new HBox(1, completedTradeLabel);
|
||||||
GridPane.setMargin(hBox2, new Insets(18, -10, -12, -10));
|
GridPane.setMargin(hBox2, new Insets(18, -10, -12, -10));
|
||||||
gridPane.getChildren().add(hBox2);
|
gridPane.getChildren().add(hBox2);
|
||||||
GridPane.setRowSpan(hBox2, 5);
|
GridPane.setRowSpan(hBox2, 5);
|
||||||
autoConfBadge.setVisible(AssetTxProofResult.COMPLETED == trade.getAssetTxProofResult());
|
|
||||||
|
|
||||||
if (trade.getDisputeState().isNotDisputed()) {
|
if (trade.getDisputeState().isNotDisputed()) {
|
||||||
addCompactTopLabelTextField(gridPane, gridRow, getBtcTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE);
|
addCompactTopLabelTextField(gridPane, gridRow, getBtcTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE);
|
||||||
|
|
|
@ -40,7 +40,6 @@ import haveno.core.payment.payload.USPostalMoneyOrderAccountPayload;
|
||||||
import haveno.core.payment.payload.WesternUnionAccountPayload;
|
import haveno.core.payment.payload.WesternUnionAccountPayload;
|
||||||
import haveno.core.trade.Contract;
|
import haveno.core.trade.Contract;
|
||||||
import haveno.core.trade.Trade;
|
import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.txproof.AssetTxProofResult;
|
|
||||||
import haveno.core.user.DontShowAgainLookup;
|
import haveno.core.user.DontShowAgainLookup;
|
||||||
import haveno.core.util.VolumeUtil;
|
import haveno.core.util.VolumeUtil;
|
||||||
import haveno.desktop.components.BusyAnimation;
|
import haveno.desktop.components.BusyAnimation;
|
||||||
|
@ -50,7 +49,6 @@ import haveno.desktop.components.indicator.TxConfidenceIndicator;
|
||||||
import haveno.desktop.main.overlays.popups.Popup;
|
import haveno.desktop.main.overlays.popups.Popup;
|
||||||
import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
|
import haveno.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
|
||||||
import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
|
import haveno.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
|
||||||
import haveno.desktop.util.GUIUtil;
|
|
||||||
import haveno.desktop.util.Layout;
|
import haveno.desktop.util.Layout;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
|
@ -67,7 +65,6 @@ import org.fxmisc.easybind.Subscription;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup;
|
import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup;
|
||||||
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
|
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
|
||||||
import static haveno.desktop.util.FormBuilder.addTitledGroupBg;
|
import static haveno.desktop.util.FormBuilder.addTitledGroupBg;
|
||||||
|
@ -154,15 +151,6 @@ public class SellerStep3View extends TradeStepView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isXmrTrade()) {
|
|
||||||
proofResultListener = (observable, oldValue, newValue) -> {
|
|
||||||
applyAssetTxProofResult(trade.getAssetTxProofResult());
|
|
||||||
};
|
|
||||||
trade.getAssetTxProofResultUpdateProperty().addListener(proofResultListener);
|
|
||||||
|
|
||||||
applyAssetTxProofResult(trade.getAssetTxProofResult());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -179,10 +167,6 @@ public class SellerStep3View extends TradeStepView {
|
||||||
if (timeoutTimer != null) {
|
if (timeoutTimer != null) {
|
||||||
timeoutTimer.stop();
|
timeoutTimer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isXmrTrade()) {
|
|
||||||
trade.getAssetTxProofResultUpdateProperty().removeListener(proofResultListener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -488,42 +472,6 @@ public class SellerStep3View extends TradeStepView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyAssetTxProofResult(@Nullable AssetTxProofResult result) {
|
|
||||||
checkNotNull(assetTxProofResultField);
|
|
||||||
checkNotNull(assetTxConfidenceIndicator);
|
|
||||||
|
|
||||||
String txt = GUIUtil.getProofResultAsString(result);
|
|
||||||
assetTxProofResultField.setText(txt);
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
assetTxConfidenceIndicator.setProgress(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (result) {
|
|
||||||
case PENDING:
|
|
||||||
case COMPLETED:
|
|
||||||
if (result.getNumRequiredConfirmations() > 0) {
|
|
||||||
int numRequiredConfirmations = result.getNumRequiredConfirmations();
|
|
||||||
int numConfirmations = result.getNumConfirmations();
|
|
||||||
if (numConfirmations == 0) {
|
|
||||||
assetTxConfidenceIndicator.setProgress(-1);
|
|
||||||
} else {
|
|
||||||
double progress = Math.min(1, (double) numConfirmations / (double) numRequiredConfirmations);
|
|
||||||
assetTxConfidenceIndicator.setProgress(progress);
|
|
||||||
assetTxConfidenceIndicator.getTooltip().setText(
|
|
||||||
Res.get("portfolio.pending.autoConf.blocks",
|
|
||||||
numConfirmations, numRequiredConfirmations));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Set invisible by default
|
|
||||||
assetTxConfidenceIndicator.setProgress(0);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Label createPopoverLabel(String text) {
|
private Label createPopoverLabel(String text) {
|
||||||
Label label = new Label(text);
|
Label label = new Label(text);
|
||||||
label.setPrefWidth(600);
|
label.setPrefWidth(600);
|
||||||
|
|
|
@ -49,7 +49,6 @@ import haveno.core.payment.PaymentAccount;
|
||||||
import haveno.core.payment.PaymentAccountList;
|
import haveno.core.payment.PaymentAccountList;
|
||||||
import haveno.core.payment.payload.PaymentMethod;
|
import haveno.core.payment.payload.PaymentMethod;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.txproof.AssetTxProofResult;
|
|
||||||
import haveno.core.user.DontShowAgainLookup;
|
import haveno.core.user.DontShowAgainLookup;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
|
@ -104,7 +103,6 @@ import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.uri.BitcoinURI;
|
import org.bitcoinj.uri.BitcoinURI;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -1018,35 +1016,6 @@ public class GUIUtil {
|
||||||
MaterialDesignIcon.APPROVAL : MaterialDesignIcon.ALERT_CIRCLE_OUTLINE;
|
MaterialDesignIcon.APPROVAL : MaterialDesignIcon.ALERT_CIRCLE_OUTLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getProofResultAsString(@Nullable AssetTxProofResult result) {
|
|
||||||
if (result == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
String key = "portfolio.pending.autoConf.state." + result.name();
|
|
||||||
switch (result) {
|
|
||||||
case UNDEFINED:
|
|
||||||
return "";
|
|
||||||
case FEATURE_DISABLED:
|
|
||||||
return Res.get(key, result.getDetails());
|
|
||||||
case TRADE_LIMIT_EXCEEDED:
|
|
||||||
return Res.get(key);
|
|
||||||
case INVALID_DATA:
|
|
||||||
return Res.get(key, result.getDetails());
|
|
||||||
case PAYOUT_TX_ALREADY_PUBLISHED:
|
|
||||||
case DISPUTE_OPENED:
|
|
||||||
case REQUESTS_STARTED:
|
|
||||||
return Res.get(key);
|
|
||||||
case PENDING:
|
|
||||||
return Res.get(key, result.getNumSuccessResults(), result.getNumRequiredSuccessResults(), result.getDetails());
|
|
||||||
case COMPLETED:
|
|
||||||
case ERROR:
|
|
||||||
case FAILED:
|
|
||||||
return Res.get(key);
|
|
||||||
default:
|
|
||||||
return result.name();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ScrollPane createScrollPane() {
|
public static ScrollPane createScrollPane() {
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
|
|
@ -1497,8 +1497,7 @@ message Trade {
|
||||||
NodeAddress refund_agent_node_address = 27;
|
NodeAddress refund_agent_node_address = 27;
|
||||||
RefundResultState refund_result_state = 28;
|
RefundResultState refund_result_state = 28;
|
||||||
string counter_currency_extra_data = 29;
|
string counter_currency_extra_data = 29;
|
||||||
string asset_tx_proof_result = 30; // name of AssetTxProofResult enum
|
string uid = 30;
|
||||||
string uid = 31;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message BuyerAsMakerTrade {
|
message BuyerAsMakerTrade {
|
||||||
|
|
Loading…
Reference in a new issue