Add basic support for validation for XMR transfer with tx key

Main part missing is the XMR proof service request processing. I did not
get the service compiled yet, so could not test response data and error
conditions.

Further it is missing a "news badge" and popup to guide the user to the
new feature.

Only basic dev tested so far.

Anyone welcome to pick the project up from here as I might not have
time soon to continue.
This commit is contained in:
chimp1984 2020-07-26 18:55:07 -05:00
parent 9ab39c2a8b
commit 43e4809d81
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
21 changed files with 869 additions and 191 deletions

View file

@ -34,12 +34,16 @@ import bisq.core.payment.payload.PaymentMethod;
import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.arbitration.TraderDataItem;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.TraderSignedWitnessMessage;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.user.User;
import bisq.network.p2p.BootstrapListener;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendMailboxMessageListener;
import bisq.network.p2p.storage.P2PDataStorage;
import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService;
@ -73,6 +77,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -852,4 +857,58 @@ public class AccountAgeWitnessService {
public Set<SignedWitness> getUnsignedSignerPubKeys() {
return signedWitnessService.getUnsignedSignerPubKeys();
}
public boolean isSignWitnessTrade(Trade trade) {
checkNotNull(trade, "trade must not be null");
checkNotNull(trade.getOffer(), "offer must not be null");
Contract contract = checkNotNull(trade.getContract());
PaymentAccountPayload sellerPaymentAccountPayload = contract.getSellerPaymentAccountPayload();
AccountAgeWitness myWitness = getMyWitness(sellerPaymentAccountPayload);
getAccountAgeWitnessUtils().witnessDebugLog(trade, myWitness);
return accountIsSigner(myWitness) &&
!peerHasSignedWitness(trade) &&
tradeAmountIsSufficient(trade.getTradeAmount());
}
public void maybeSignWitness(Trade trade) {
if (isSignWitnessTrade(trade)) {
var signedWitnessOptional = traderSignPeersAccountAgeWitness(trade);
signedWitnessOptional.ifPresent(signedWitness -> sendSignedWitnessToPeer(signedWitness, trade));
}
}
private void sendSignedWitnessToPeer(SignedWitness signedWitness, Trade trade) {
if (trade == null) return;
NodeAddress tradingPeerNodeAddress = trade.getTradingPeerNodeAddress();
var traderSignedWitnessMessage = new TraderSignedWitnessMessage(UUID.randomUUID().toString(), trade.getId(),
tradingPeerNodeAddress, signedWitness);
p2PService.sendEncryptedMailboxMessage(
tradingPeerNodeAddress,
trade.getProcessModel().getTradingPeer().getPubKeyRing(),
traderSignedWitnessMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("SendMailboxMessageListener onArrived tradeId={} at peer {} SignedWitness {}",
trade.getId(), tradingPeerNodeAddress, signedWitness);
}
@Override
public void onStoredInMailbox() {
log.info("SendMailboxMessageListener onStoredInMailbox tradeId={} at peer {} SignedWitness {}",
trade.getId(), tradingPeerNodeAddress, signedWitness);
}
@Override
public void onFault(String errorMessage) {
log.error("SendMailboxMessageListener onFault tradeId={} at peer {} SignedWitness {}",
trade.getId(), tradingPeerNodeAddress, signedWitness);
}
}
);
}
}

View file

@ -428,6 +428,13 @@ public abstract class Trade implements Tradable, Model {
private long refreshInterval;
private static final long MAX_REFRESH_INTERVAL = 4 * ChronoUnit.HOURS.getDuration().toMillis();
// Added in v1.3.7
// We use that for the XMR txKey but want to keep it generic to be flexible for other payment methods or assets.
@Getter
@Setter
private String counterCurrencyExtraData;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization
///////////////////////////////////////////////////////////////////////////////////////////
@ -538,6 +545,8 @@ public abstract class Trade implements Tradable, Model {
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
Optional.ofNullable(delayedPayoutTxBytes).ifPresent(e -> builder.setDelayedPayoutTxBytes(ByteString.copyFrom(delayedPayoutTxBytes)));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
return builder.build();
}
@ -570,6 +579,7 @@ public abstract class Trade implements Tradable, Model {
trade.setDelayedPayoutTxBytes(ProtoUtil.byteArrayOrNullFromProto(proto.getDelayedPayoutTxBytes()));
trade.setLockTime(proto.getLockTime());
trade.setLastRefreshRequestDate(proto.getLastRefreshRequestDate());
trade.setCounterCurrencyExtraData(ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()));
trade.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)

View file

@ -21,6 +21,7 @@ import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.exceptions.AddressEntryException;
import bisq.core.btc.exceptions.TxBroadcastException;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
@ -34,10 +35,15 @@ import bisq.core.offer.OfferPayload;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
import bisq.core.offer.availability.OfferAvailabilityModel;
import bisq.core.payment.payload.AssetsAccountPayload;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.asset.xmr.XmrProofResult;
import bisq.core.trade.asset.xmr.XmrProofResultWithTradeId;
import bisq.core.trade.asset.xmr.XmrTransferProofService;
import bisq.core.trade.closed.ClosedTradableManager;
import bisq.core.trade.failed.FailedTradesManager;
import bisq.core.trade.handlers.TradeResultHandler;
@ -46,6 +52,7 @@ import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.statistics.ReferralIdService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.Validator;
@ -65,8 +72,6 @@ import bisq.common.handlers.ResultHandler;
import bisq.common.proto.network.NetworkEnvelope;
import bisq.common.proto.persistable.PersistedDataHost;
import bisq.common.storage.Storage;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
@ -81,8 +86,10 @@ import com.google.common.util.concurrent.FutureCallback;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
@ -93,7 +100,6 @@ import org.spongycastle.crypto.params.KeyParameter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@ -111,6 +117,8 @@ import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
public class TradeManager implements PersistedDataHost {
private static final Logger log = LoggerFactory.getLogger(TradeManager.class);
@ -147,8 +155,14 @@ public class TradeManager implements PersistedDataHost {
@Getter
private final ObservableList<Trade> tradesWithoutDepositTx = FXCollections.observableArrayList();
private final DumpDelayedPayoutTx dumpDelayedPayoutTx;
private final XmrTransferProofService xmrTransferProofService;
private final WalletsSetup walletsSetup;
private final Preferences preferences;
@Getter
private final boolean allowFaultyDelayedTxs;
// This observable property can be used for UI to show a notification to user in case a XMR txKey was reused.
@Getter
private final ObjectProperty<XmrProofResultWithTradeId> proofResultWithTradeIdProperty = new SimpleObjectProperty<>();
///////////////////////////////////////////////////////////////////////////////////////////
@ -177,6 +191,9 @@ public class TradeManager implements PersistedDataHost {
ClockWatcher clockWatcher,
Storage<TradableList<Trade>> storage,
DumpDelayedPayoutTx dumpDelayedPayoutTx,
XmrTransferProofService xmrTransferProofService,
WalletsSetup walletsSetup,
Preferences preferences,
@Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) {
this.user = user;
this.keyRing = keyRing;
@ -198,6 +215,9 @@ public class TradeManager implements PersistedDataHost {
this.daoFacade = daoFacade;
this.clockWatcher = clockWatcher;
this.dumpDelayedPayoutTx = dumpDelayedPayoutTx;
this.xmrTransferProofService = xmrTransferProofService;
this.walletsSetup = walletsSetup;
this.preferences = preferences;
this.allowFaultyDelayedTxs = allowFaultyDelayedTxs;
tradableListStorage = storage;
@ -855,4 +875,121 @@ public class TradeManager implements PersistedDataHost {
public void persistTrades() {
tradableList.persist();
}
public void processCounterCurrencyExtraData(Trade trade) {
String counterCurrencyExtraData = trade.getCounterCurrencyExtraData();
if (counterCurrencyExtraData == null || counterCurrencyExtraData.isEmpty()) {
return;
}
String txHash = trade.getCounterCurrencyTxId();
if (txHash == null || txHash.isEmpty()) {
return;
}
Contract contract = checkNotNull(trade.getContract(), "contract must not be null");
PaymentAccountPayload sellersPaymentAccountPayload = contract.getSellerPaymentAccountPayload();
if (!(sellersPaymentAccountPayload instanceof AssetsAccountPayload)) {
return;
}
AssetsAccountPayload sellersAssetsAccountPayload = (AssetsAccountPayload) sellersPaymentAccountPayload;
if (!(trade instanceof SellerTrade)) {
return;
}
Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null");
if (offer.getCurrencyCode().equals("XMR")) {
String txKey = counterCurrencyExtraData;
// 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(tradableList.stream(), failedTradesManager.getFailedTrades().stream());
Stream<Trade> closedTrades = closedTradableManager.getClosedTradables().stream()
.filter(tradable -> tradable instanceof Trade)
.map(tradable -> (Trade) tradable);
Stream<Trade> allTrades = Stream.concat(failedAndOpenTrades, closedTrades);
boolean txKeyUsedAtAnyOpenTrade = 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) {
String message = "Peer used the XMR tx key already at another trade with trade ID " +
t.getId() + ". This might be a scam attempt.";
log.warn(message);
proofResultWithTradeIdProperty.set(new XmrProofResultWithTradeId(XmrProofResult.TX_KEY_REUSED, trade.getId()));
}
return alreadyUsed;
});
if (txKeyUsedAtAnyOpenTrade) {
return;
}
if (preferences.isAutoConfirmXmr()) {
String address = sellersAssetsAccountPayload.getAddress();
//TODO for dev testing
address = "85q13WDADXE26W6h7cStpPMkn8tWpvWgHbpGWWttFEafGXyjsBTXxxyQms4UErouTY5sdKpYHVjQm6SagiCqytseDkzfgub";
// 8.90259736 is dev test value
long amount = (long) Float.parseFloat("8.90259736") * 100000000; // todo check XMR denomination
xmrTransferProofService.requestProof(trade.getId(),
txHash,
txKey,
address,
amount,
result -> {
switch (result) {
case TX_NOT_CONFIRMED:
// Repeating the requests is handled in XmrTransferProofRequester
break;
case PROOF_OK:
if (!p2PService.isBootstrapped()) {
return;
}
if (!walletsSetup.hasSufficientPeersForBroadcast()) {
return;
}
if (!walletsSetup.isDownloadComplete()) {
return;
}
if (!trade.isPayoutPublished()) {
trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT);
}
accountAgeWitnessService.maybeSignWitness(trade);
((SellerTrade) trade).onFiatPaymentReceived(() -> {
}, errorMessage -> {
});
break;
case UNKNOWN_ERROR:
case TX_KEY_REUSED:
case TX_NEVER_FOUND:
case TX_HASH_INVALID:
case TX_KEY_INVALID:
case ADDRESS_INVALID:
case AMOUNT_NOT_MATCHING:
case PROOF_FAILED:
default:
log.error("Case not handled. " + result);
break;
}
proofResultWithTradeIdProperty.set(new XmrProofResultWithTradeId(result, trade.getId()));
},
(errorMsg, throwable) -> {
log.warn(errorMsg);
});
}
}
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.asset.xmr;
public enum XmrProofResult {
TX_NOT_CONFIRMED,
PROOF_OK,
UNKNOWN_ERROR,
TX_KEY_REUSED,
TX_NEVER_FOUND,
TX_HASH_INVALID,
TX_KEY_INVALID,
ADDRESS_INVALID,
AMOUNT_NOT_MATCHING,
PROOF_FAILED
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.asset.xmr;
import lombok.Value;
@Value
public class XmrProofResultWithTradeId {
private final XmrProofResult xmrProofResult;
private final String tradeId;
public XmrProofResultWithTradeId(XmrProofResult xmrProofResult, String tradeId) {
this.xmrProofResult = xmrProofResult;
this.tradeId = tradeId;
}
}

View file

@ -0,0 +1,158 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.asset.xmr;
import bisq.common.UserThread;
import bisq.common.app.Version;
import bisq.common.handlers.FaultHandler;
import bisq.common.util.Utilities;
import javax.inject.Singleton;
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 java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@Slf4j
@Singleton
class XmrTransferProofRequester {
private final ListeningExecutorService executorService = Utilities.getListeningExecutorService(
"XmrTransferProofService", 3, 5, 10 * 60);
private final XmrTxProofHttpClient httpClient;
private final String txHash;
private final String txKey;
private final String recipientAddress;
private final long amount;
private final Consumer<XmrProofResult> resultHandler;
private final FaultHandler faultHandler;
private long firstRequest;
//todo dev settings
private long REPEAT_REQUEST_SEC = TimeUnit.SECONDS.toMillis(5);
private long MAX_REQUEST_PERIOD = TimeUnit.HOURS.toMillis(12);
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
XmrTransferProofRequester(XmrTxProofHttpClient httpClient,
String txHash,
String txKey,
String recipientAddress,
long amount,
Consumer<XmrProofResult> resultHandler,
FaultHandler faultHandler) {
this.httpClient = httpClient;
this.txHash = txHash;
this.txKey = txKey;
this.recipientAddress = recipientAddress;
this.amount = amount;
this.resultHandler = resultHandler;
this.faultHandler = faultHandler;
firstRequest = System.currentTimeMillis();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void request() {
// todo dev test address for a real tx proof
/*
txID: 5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802
txKey: f3ce66c9d395e5e460c8802b2c3c1fff04e508434f9738ee35558aac4678c906
address: 85q13WDADXE26W6h7cStpPMkn8tWpvWgHbpGWWttFEafGXyjsBTXxxyQms4UErouTY5sdKpYHVjQm6SagiCqytseDkzfgub
ammount : 8.90259736 XMR
*/
ListenableFuture<XmrProofResult> future = executorService.submit(() -> {
Thread.currentThread().setName("XmrTransferProofRequest-" + this.toString());
String param = "/api/outputs?txhash=" + txHash +
"&address=" + recipientAddress +
"&viewkey=" + txKey +
"&txprove=1";
String json = httpClient.requestWithGET(param, "User-Agent", "bisq/" + Version.VERSION);
Thread.sleep(3000);
//
return parseResult(json);
});
Futures.addCallback(future, new FutureCallback<>() {
public void onSuccess(XmrProofResult result) {
if (result == XmrProofResult.TX_NOT_CONFIRMED && System.currentTimeMillis() - firstRequest < MAX_REQUEST_PERIOD) {
UserThread.runAfter(() -> request(), REPEAT_REQUEST_SEC);
} else {
UserThread.execute(() -> resultHandler.accept(result));
}
}
public void onFailure(@NotNull Throwable throwable) {
String errorMessage = "Request to " + httpClient.getBaseUrl() + " failed";
faultHandler.handleFault(errorMessage, throwable);
}
});
}
private XmrProofResult parseResult(String json) {
//TODO parse json
//TODO need service to check diff. error conditions
return XmrProofResult.PROOF_OK;
// check recipientAddress and amount
// json example
/*
{
"data": {
"address": "42f18fc61586554095b0799b5c4b6f00cdeb26a93b20540d366932c6001617b75db35109fbba7d5f275fef4b9c49e0cc1c84b219ec6ff652fda54f89f7f63c88",
"outputs": [
{
"amount": 34980000000000,
"match": true,
"output_idx": 0,
"output_pubkey": "35d7200229e725c2bce0da3a2f20ef0720d242ecf88bfcb71eff2025c2501fdb"
},
{
"amount": 0,
"match": false,
"output_idx": 1,
"output_pubkey": "44efccab9f9b42e83c12da7988785d6c4eb3ec6e7aa2ae1234e2f0f7cb9ed6dd"
}
],
"tx_hash": "17049bc5f2d9fbca1ce8dae443bbbbed2fc02f1ee003ffdd0571996905faa831",
"tx_prove": false,
"viewkey": "f359631075708155cc3d92a32b75a7d02a5dcf27756707b47a2b31b21c389501"
},
"status": "success"
}
*/
}
}

View file

@ -0,0 +1,78 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.asset.xmr;
import bisq.common.handlers.FaultHandler;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
/**
* Manages the XMR transfers proof requests for multiple trades.
*/
@Slf4j
public class XmrTransferProofService {
private final XmrTxProofHttpClient httpClient;
private Map<String, XmrTransferProofRequester> map = new HashMap<>();
@Inject
public XmrTransferProofService(XmrTxProofHttpClient httpClient) {
this.httpClient = httpClient;
//this.httpClient.setBaseUrl("http://139.59.140.37:8081");
this.httpClient.setBaseUrl("http://127.0.0.1:8081");
this.httpClient.setIgnoreSocks5Proxy(false);
}
public void requestProof(String tradeId,
String txHash,
String txKey,
String recipientAddress,
long amount,
Consumer<XmrProofResult> resultHandler,
FaultHandler faultHandler) {
if (map.containsKey(tradeId)) {
log.warn("We started a proof request for trade with ID {} already", tradeId);
return;
}
XmrTransferProofRequester requester = new XmrTransferProofRequester(httpClient,
txHash,
txKey,
recipientAddress,
amount,
result -> {
cleanup(tradeId);
resultHandler.accept(result);
},
(errorMsg, throwable) -> {
cleanup(tradeId);
faultHandler.handleFault(errorMsg, throwable);
});
map.put(tradeId, requester);
requester.request();
}
private void cleanup(String tradeId) {
map.remove(tradeId);
}
}

View file

@ -0,0 +1,32 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.asset.xmr;
import bisq.network.Socks5ProxyProvider;
import bisq.network.http.HttpClient;
import javax.inject.Inject;
import javax.annotation.Nullable;
public class XmrTxProofHttpClient extends HttpClient {
@Inject
public XmrTxProofHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) {
super(socks5ProxyProvider);
}
}

View file

@ -21,6 +21,7 @@ import bisq.network.p2p.MailboxMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import bisq.common.proto.ProtoUtil;
import bisq.common.util.Utilities;
import com.google.protobuf.ByteString;
@ -41,17 +42,24 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMessage im
@Nullable
private final String counterCurrencyTxId;
// Added in v1.3.7
// We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets.
@Nullable
private String counterCurrencyExtraData;
public CounterCurrencyTransferStartedMessage(String tradeId,
String buyerPayoutAddress,
NodeAddress senderNodeAddress,
byte[] buyerSignature,
@Nullable String counterCurrencyTxId,
@Nullable String counterCurrencyExtraData,
String uid) {
this(tradeId,
buyerPayoutAddress,
senderNodeAddress,
buyerSignature,
counterCurrencyTxId,
counterCurrencyExtraData,
uid,
Version.getP2PMessageVersion());
}
@ -66,6 +74,7 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMessage im
NodeAddress senderNodeAddress,
byte[] buyerSignature,
@Nullable String counterCurrencyTxId,
@Nullable String counterCurrencyExtraData,
String uid,
int messageVersion) {
super(messageVersion, tradeId, uid);
@ -73,6 +82,7 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMessage im
this.senderNodeAddress = senderNodeAddress;
this.buyerSignature = buyerSignature;
this.counterCurrencyTxId = counterCurrencyTxId;
this.counterCurrencyExtraData = counterCurrencyExtraData;
}
@Override
@ -85,16 +95,19 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMessage im
.setUid(uid);
Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
return getNetworkEnvelopeBuilder().setCounterCurrencyTransferStartedMessage(builder).build();
}
public static CounterCurrencyTransferStartedMessage fromProto(protobuf.CounterCurrencyTransferStartedMessage proto, int messageVersion) {
public static CounterCurrencyTransferStartedMessage fromProto(protobuf.CounterCurrencyTransferStartedMessage proto,
int messageVersion) {
return new CounterCurrencyTransferStartedMessage(proto.getTradeId(),
proto.getBuyerPayoutAddress(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getBuyerSignature().toByteArray(),
proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId(),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()),
proto.getUid(),
messageVersion);
}
@ -106,6 +119,7 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMessage im
"\n buyerPayoutAddress='" + buyerPayoutAddress + '\'' +
",\n senderNodeAddress=" + senderNodeAddress +
",\n counterCurrencyTxId=" + counterCurrencyTxId +
",\n counterCurrencyExtraData=" + counterCurrencyExtraData +
",\n uid='" + uid + '\'' +
",\n buyerSignature=" + Utilities.bytesAsHexString(buyerSignature) +
"\n} " + super.toString();

View file

@ -54,6 +54,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends TradeTask {
processModel.getMyNodeAddress(),
processModel.getPayoutTxSignature(),
trade.getCounterCurrencyTxId(),
trade.getCounterCurrencyExtraData(),
UUID.randomUUID().toString()
);
NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress();

View file

@ -49,7 +49,17 @@ public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTas
// update to the latest peer address of our peer if the message is correct
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
trade.setCounterCurrencyTxId(message.getCounterCurrencyTxId());
String counterCurrencyTxId = message.getCounterCurrencyTxId();
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) {
trade.setCounterCurrencyTxId(counterCurrencyTxId);
}
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) {
trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
processModel.getTradeManager().processCounterCurrencyExtraData(trade);
}
processModel.removeMailboxMessageAfterProcessing(trade);
trade.setState(Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG);

View file

@ -17,8 +17,8 @@
package bisq.core.user;
import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.core.btc.nodes.BtcNodes;
import bisq.core.btc.nodes.LocalBitcoinNode;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.locale.Country;
import bisq.core.locale.CountryUtil;
@ -394,6 +394,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
persist();
}
public void setAutoConfirmXmr(boolean autoConfirmXmr) {
prefPayload.setAutoConfirmXmr(autoConfirmXmr);
persist();
}
private void persist() {
if (initialReadDone)
storage.queueUpForSave(prefPayload);
@ -965,5 +970,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
int getBlockNotifyPort();
void setTacAcceptedV120(boolean tacAccepted);
void setAutoConfirmXmr(boolean autoConfirmXmr);
}
}

View file

@ -127,6 +127,9 @@ public final class PreferencesPayload implements UserThreadMappedPersistableEnve
private int blockNotifyPort;
private boolean tacAcceptedV120;
// Added with 1.3.7 false be default
private boolean autoConfirmXmr;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -186,7 +189,8 @@ public final class PreferencesPayload implements UserThreadMappedPersistableEnve
.setIgnoreDustThreshold(ignoreDustThreshold)
.setBuyerSecurityDepositAsPercentForCrypto(buyerSecurityDepositAsPercentForCrypto)
.setBlockNotifyPort(blockNotifyPort)
.setTacAcceptedV120(tacAcceptedV120);
.setTacAcceptedV120(tacAcceptedV120)
.setAutoConfirmXmr(autoConfirmXmr);
Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory);
Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage()));
Optional.ofNullable(offerBookChartScreenCurrencyCode).ifPresent(builder::setOfferBookChartScreenCurrencyCode);
@ -274,6 +278,7 @@ public final class PreferencesPayload implements UserThreadMappedPersistableEnve
proto.getIgnoreDustThreshold(),
proto.getBuyerSecurityDepositAsPercentForCrypto(),
proto.getBlockNotifyPort(),
proto.getTacAcceptedV120());
proto.getTacAcceptedV120(),
proto.getAutoConfirmXmr());
}
}

View file

@ -718,6 +718,9 @@ portfolio.pending.step3_seller.amountToReceive=Amount to receive
portfolio.pending.step3_seller.yourAddress=Your {0} address
portfolio.pending.step3_seller.buyersAddress=Buyers {0} address
portfolio.pending.step3_seller.yourAccount=Your trading account
portfolio.pending.step3_seller.xmrTxHash=Tx hash
portfolio.pending.step3_seller.xmrTxKey=Tx private key
portfolio.pending.step3_seller.xmrTxVerificationError=The XMR transfer validation for trade with ID ''{0}'' failed.\nReason: ''{1}''
portfolio.pending.step3_seller.buyersAccount=Buyers trading account
portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt
portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1}
@ -1051,6 +1054,7 @@ setting.preferences.explorer=Bitcoin block explorer
setting.preferences.explorer.bsq=BSQ block explorer
setting.preferences.deviation=Max. deviation from market price
setting.preferences.avoidStandbyMode=Avoid standby mode
setting.preferences.autoConfirmXMR=Use XMR tx proof tool
setting.preferences.deviationToLarge=Values higher than {0}% are not allowed.
setting.preferences.txFee=Withdrawal transaction fee (satoshis/byte)
setting.preferences.useCustomValue=Use custom value
@ -2488,6 +2492,10 @@ sendPrivateNotificationWindow.send=Send private notification
showWalletDataWindow.walletData=Wallet data
showWalletDataWindow.includePrivKeys=Include private keys
setXMRTxKeyWindow.headline=Prove sending of XMR
setXMRTxKeyWindow.txHash=Transaction hash
setXMRTxKeyWindow.txKey=Tx private key
# We do not translate the tac because of the legal nature. We would need translations checked by lawyers
# in each language which is too expensive atm.
tacWindow.headline=User agreement

View file

@ -0,0 +1,101 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays.windows;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.core.locale.Res;
import bisq.common.UserThread;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.addInputTextField;
import static javafx.beans.binding.Bindings.createBooleanBinding;
public class SetXmrTxKeyWindow extends Overlay<SetXmrTxKeyWindow> {
private InputTextField txHashInputTextField, txKeyInputTextField;
public SetXmrTxKeyWindow() {
type = Type.Attention;
}
public void show() {
if (headLine == null)
headLine = Res.get("setXMRTxKeyWindow.headline");
width = 868;
createGridPane();
addHeadLine();
addContent();
addButtons();
actionButton.disableProperty().bind(createBooleanBinding(() ->
txHashInputTextField.getText().isEmpty() || txKeyInputTextField.getText().isEmpty(),
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() {
txHashInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.txHash"), 10);
txKeyInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("setXMRTxKeyWindow.txKey"));
UserThread.runAfter(() -> {
//todo: remove dev test data
txHashInputTextField.setText("5e665addf6d7c6300670e8a89564ed12b5c1a21c336408e2835668f9a6a0d802");
txKeyInputTextField.setText("f3ce66c9d395e5e460c8802b2c3c1fff04e508434f9738ee35558aac4678c906");
}, 200, TimeUnit.MILLISECONDS);
}
}

View file

@ -188,8 +188,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
final Trade trade = getTrade();
checkNotNull(trade, "trade must not be null");
checkArgument(trade instanceof BuyerTrade, "Check failed: trade instanceof BuyerTrade");
// TODO UI not impl yet
trade.setCounterCurrencyTxId("");
((BuyerTrade) trade).onFiatPaymentStarted(resultHandler, errorMessageHandler);
}
@ -703,5 +701,13 @@ public class PendingTradesDataModel extends ActivatableDataModel {
public void addTradeToFailedTrades() {
tradeManager.addTradeToFailedTrades(selectedTrade);
}
public boolean isSignWitnessTrade() {
return accountAgeWitnessService.isSignWitnessTrade(selectedTrade);
}
public void maybeSignWitness() {
accountAgeWitnessService.maybeSignWitness(selectedTrade);
}
}

View file

@ -22,8 +22,6 @@ import bisq.desktop.common.model.ViewModel;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.witness.AccountAgeWitness;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.locale.CurrencyUtil;
@ -34,17 +32,13 @@ import bisq.core.provider.fee.FeeService;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.closed.ClosedTradableManager;
import bisq.core.trade.messages.RefreshTradeStateRequest;
import bisq.core.trade.messages.TraderSignedWitnessMessage;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.validation.BtcAddressValidator;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.SendMailboxMessageListener;
import bisq.common.ClockWatcher;
import bisq.common.app.DevEnv;
@ -63,7 +57,6 @@ import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.util.Date;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.Getter;
@ -370,63 +363,6 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
.size();
}
///////////////////////////////////////////////////////////////////////////////////////////
// AccountAgeWitness signing
///////////////////////////////////////////////////////////////////////////////////////////
public boolean isSignWitnessTrade() {
checkNotNull(trade, "trade must not be null");
checkNotNull(trade.getOffer(), "offer must not be null");
AccountAgeWitness myWitness = accountAgeWitnessService.getMyWitness(dataModel.getSellersPaymentAccountPayload());
accountAgeWitnessService.getAccountAgeWitnessUtils().witnessDebugLog(trade, myWitness);
return accountAgeWitnessService.accountIsSigner(myWitness) &&
!accountAgeWitnessService.peerHasSignedWitness(trade) &&
accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount());
}
public void maybeSignWitness() {
if (isSignWitnessTrade()) {
var signedWitness = accountAgeWitnessService.traderSignPeersAccountAgeWitness(trade);
signedWitness.ifPresent(this::sendSignedWitnessToPeer);
}
}
private void sendSignedWitnessToPeer(SignedWitness signedWitness) {
Trade trade = getTrade();
if (trade == null) return;
NodeAddress tradingPeerNodeAddress = trade.getTradingPeerNodeAddress();
var traderSignedWitnessMessage = new TraderSignedWitnessMessage(UUID.randomUUID().toString(), trade.getId(),
tradingPeerNodeAddress, signedWitness);
p2PService.sendEncryptedMailboxMessage(
tradingPeerNodeAddress,
trade.getProcessModel().getTradingPeer().getPubKeyRing(),
traderSignedWitnessMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("SendMailboxMessageListener onArrived tradeId={} at peer {} SignedWitness {}",
trade.getId(), tradingPeerNodeAddress, signedWitness);
}
@Override
public void onStoredInMailbox() {
log.info("SendMailboxMessageListener onStoredInMailbox tradeId={} at peer {} SignedWitness {}",
trade.getId(), tradingPeerNodeAddress, signedWitness);
}
@Override
public void onFault(String errorMessage) {
log.error("SendMailboxMessageListener onFault tradeId={} at peer {} SignedWitness {}",
trade.getId(), tradingPeerNodeAddress, signedWitness);
}
}
);
}
///////////////////////////////////////////////////////////////////////////////////////////
// States

View file

@ -48,6 +48,7 @@ import bisq.desktop.components.paymentmethods.UpholdForm;
import bisq.desktop.components.paymentmethods.WeChatPayForm;
import bisq.desktop.components.paymentmethods.WesternUnionForm;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.SetXmrTxKeyWindow;
import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
import bisq.desktop.util.DisplayUtils;
@ -384,73 +385,93 @@ public class BuyerStep2View extends TradeStepView {
///////////////////////////////////////////////////////////////////////////////////////////
private void onPaymentStarted() {
if (model.dataModel.isBootstrappedOrShowPopup()) {
if (model.dataModel.getSellersPaymentAccountPayload() instanceof CashDepositAccountPayload) {
String key = "confirmPaperReceiptSent";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.paperReceipt.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.paperReceipt.msg"))
.onAction(this::showConfirmPaymentStartedPopup)
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (model.dataModel.getSellersPaymentAccountPayload() instanceof WesternUnionAccountPayload) {
String key = "westernUnionMTCNSent";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
String email = ((WesternUnionAccountPayload) model.dataModel.getSellersPaymentAccountPayload()).getEmail();
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg", email))
.onAction(this::showConfirmPaymentStartedPopup)
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (model.dataModel.getSellersPaymentAccountPayload() instanceof MoneyGramAccountPayload) {
String key = "moneyGramMTCNSent";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
String email = ((MoneyGramAccountPayload) model.dataModel.getSellersPaymentAccountPayload()).getEmail();
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg", email))
.onAction(this::showConfirmPaymentStartedPopup)
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (model.dataModel.getSellersPaymentAccountPayload() instanceof HalCashAccountPayload) {
String key = "halCashCodeInfo";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
String mobileNr = ((HalCashAccountPayload) model.dataModel.getSellersPaymentAccountPayload()).getMobileNr();
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.halCashInfo.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.halCashInfo.msg",
model.dataModel.getTrade().getShortId(), mobileNr))
.onAction(this::showConfirmPaymentStartedPopup)
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
if (!model.dataModel.isBootstrappedOrShowPopup()) {
return;
}
PaymentAccountPayload sellersPaymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
Trade trade = checkNotNull(model.dataModel.getTrade(), "trade must not be null");
if (sellersPaymentAccountPayload instanceof CashDepositAccountPayload) {
String key = "confirmPaperReceiptSent";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.paperReceipt.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.paperReceipt.msg"))
.onAction(this::showConfirmPaymentStartedPopup)
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (sellersPaymentAccountPayload instanceof WesternUnionAccountPayload) {
String key = "westernUnionMTCNSent";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
String email = ((WesternUnionAccountPayload) sellersPaymentAccountPayload).getEmail();
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.westernUnionMTCNInfo.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.westernUnionMTCNInfo.msg", email))
.onAction(this::showConfirmPaymentStartedPopup)
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (sellersPaymentAccountPayload instanceof MoneyGramAccountPayload) {
String key = "moneyGramMTCNSent";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
String email = ((MoneyGramAccountPayload) sellersPaymentAccountPayload).getEmail();
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.moneyGramMTCNInfo.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.moneyGramMTCNInfo.msg", email))
.onAction(this::showConfirmPaymentStartedPopup)
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (sellersPaymentAccountPayload instanceof HalCashAccountPayload) {
String key = "halCashCodeInfo";
if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) {
String mobileNr = ((HalCashAccountPayload) sellersPaymentAccountPayload).getMobileNr();
Popup popup = new Popup();
popup.headLine(Res.get("portfolio.pending.step2_buyer.halCashInfo.headline"))
.feedback(Res.get("portfolio.pending.step2_buyer.halCashInfo.msg",
trade.getShortId(), mobileNr))
.onAction(this::showConfirmPaymentStartedPopup)
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.onClose(popup::hide)
.dontShowAgainId(key)
.show();
} else {
showConfirmPaymentStartedPopup();
}
} else if (sellersPaymentAccountPayload instanceof AssetsAccountPayload) {
Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null");
if (offer.getCurrencyCode().equals("XMR")) {
SetXmrTxKeyWindow setXmrTxKeyWindow = new SetXmrTxKeyWindow();
setXmrTxKeyWindow.actionButtonText(Res.get("portfolio.pending.step2_buyer.confirmStart.headline"))
.onAction(() -> {
String txKey = setXmrTxKeyWindow.getTxKey();
String txHash = setXmrTxKeyWindow.getTxHash();
trade.setCounterCurrencyExtraData(txKey);
trade.setCounterCurrencyTxId(txHash);
showConfirmPaymentStartedPopup();
})
.closeButtonText(Res.get("shared.cancel"))
.onClose(setXmrTxKeyWindow::hide)
.show();
}
} else {
showConfirmPaymentStartedPopup();
}
}

View file

@ -42,6 +42,7 @@ import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload;
import bisq.core.payment.payload.WesternUnionAccountPayload;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.asset.xmr.XmrProofResultWithTradeId;
import bisq.core.user.DontShowAgainLookup;
import bisq.common.Timer;
@ -59,6 +60,8 @@ import javafx.scene.layout.Priority;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.value.ChangeListener;
import java.util.Optional;
import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup;
@ -73,6 +76,8 @@ public class SellerStep3View extends TradeStepView {
private BusyAnimation busyAnimation;
private Subscription tradeStatePropertySubscription;
private Timer timeoutTimer;
private final ChangeListener<XmrProofResultWithTradeId> xmrProofResultWithTradeIdListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, Initialisation
@ -80,6 +85,10 @@ public class SellerStep3View extends TradeStepView {
public SellerStep3View(PendingTradesViewModel model) {
super(model);
xmrProofResultWithTradeIdListener = (observable, oldValue, newValue) -> {
processXmrProofResult(newValue);
};
}
@Override
@ -139,6 +148,9 @@ public class SellerStep3View extends TradeStepView {
}
}
});
model.dataModel.tradeManager.getProofResultWithTradeIdProperty().addListener(xmrProofResultWithTradeIdListener);
processXmrProofResult(model.dataModel.tradeManager.getProofResultWithTradeIdProperty().get());
}
@Override
@ -154,6 +166,9 @@ public class SellerStep3View extends TradeStepView {
if (timeoutTimer != null)
timeoutTimer.stop();
model.dataModel.tradeManager.getProofResultWithTradeIdProperty().removeListener(xmrProofResultWithTradeIdListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -216,6 +231,20 @@ public class SellerStep3View extends TradeStepView {
peersPaymentDetailsTextField.setMouseTransparent(false);
peersPaymentDetailsTextField.setTooltip(new Tooltip(peersPaymentDetails));
String counterCurrencyTxId = trade.getCounterCurrencyTxId();
String counterCurrencyExtraData = trade.getCounterCurrencyExtraData();
if (counterCurrencyTxId != null && !counterCurrencyTxId.isEmpty() &&
counterCurrencyExtraData != null && !counterCurrencyExtraData.isEmpty()) {
TextFieldWithCopyIcon txHashTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow,
0, Res.get("portfolio.pending.step3_seller.xmrTxHash"), counterCurrencyTxId).second;
txHashTextField.setMouseTransparent(false);
txHashTextField.setTooltip(new Tooltip(myPaymentDetails));
TextFieldWithCopyIcon txKeyDetailsTextField = addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow,
1, Res.get("portfolio.pending.step3_seller.xmrTxKey"), counterCurrencyExtraData).second;
txKeyDetailsTextField.setMouseTransparent(false);
txKeyDetailsTextField.setTooltip(new Tooltip(peersPaymentDetails));
}
Tuple4<Button, BusyAnimation, Label, HBox> tuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++gridRow,
Res.get("portfolio.pending.step3_seller.confirmReceipt"));
@ -294,7 +323,7 @@ public class SellerStep3View extends TradeStepView {
}
}
message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.note");
if (model.isSignWitnessTrade()) {
if (model.dataModel.isSignWitnessTrade()) {
message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer");
}
new Popup()
@ -351,7 +380,7 @@ public class SellerStep3View extends TradeStepView {
message += Res.get("portfolio.pending.step3_seller.bankCheck", optionalHolderName.get(), part);
}
if (model.isSignWitnessTrade()) {
if (model.dataModel.isSignWitnessTrade()) {
message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer");
}
}
@ -370,7 +399,7 @@ public class SellerStep3View extends TradeStepView {
if (!trade.isPayoutPublished())
trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT);
model.maybeSignWitness();
model.dataModel.maybeSignWitness();
model.dataModel.onFiatPaymentReceived(() -> {
// In case the first send failed we got the support button displayed.
@ -406,6 +435,47 @@ public class SellerStep3View extends TradeStepView {
protected void deactivatePaymentButtons(boolean isDisabled) {
confirmButton.setDisable(isDisabled);
}
private void processXmrProofResult(XmrProofResultWithTradeId result) {
if (result == null) {
return;
}
Trade trade = model.dataModel.getTrade();
if (trade == null) {
return;
}
if (result.getTradeId().equals(trade.getId())) {
boolean hasFailed;
switch (result.getXmrProofResult()) {
case TX_NOT_CONFIRMED:
case PROOF_OK:
hasFailed = false;
break;
case UNKNOWN_ERROR:
case TX_KEY_REUSED:
case TX_NEVER_FOUND:
case TX_HASH_INVALID:
case TX_KEY_INVALID:
case ADDRESS_INVALID:
case AMOUNT_NOT_MATCHING:
case PROOF_FAILED:
default:
hasFailed = true;
break;
}
if (hasFailed) {
// We don't show yet translated messages for the diff. error cases but the ENUM name.
new Popup().warning(Res.get("portfolio.pending.step3_seller.xmrTxVerificationError",
result.getTradeId(),
result.getXmrProofResult().toString()))
.width(800)
.show();
}
}
}
}

View file

@ -100,9 +100,6 @@ import static com.google.common.base.Preconditions.checkArgument;
@FxmlView
public class PreferencesView extends ActivatableViewAndModel<GridPane, PreferencesViewModel> {
// not supported yet
//private ComboBox<String> btcDenominationComboBox;
private ComboBox<BlockChainExplorer> blockChainExplorerComboBox;
private ComboBox<BlockChainExplorer> bsqBlockChainExplorerComboBox;
private ComboBox<String> userLanguageComboBox;
@ -110,7 +107,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
private ComboBox<TradeCurrency> preferredTradeCurrencyComboBox;
private ToggleButton showOwnOffersInOfferBook, useAnimations, useDarkMode, sortMarketCurrenciesNumerically,
avoidStandbyMode, useCustomFee;
avoidStandbyMode, useCustomFee, autoConfirmXmr;
private int gridRow = 0;
private InputTextField transactionFeeInputTextField, ignoreTradersListInputTextField, ignoreDustThresholdInputTextField,
/*referralIdInputTextField,*/
@ -133,7 +130,6 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
private ListView<CryptoCurrency> cryptoCurrenciesListView;
private ComboBox<CryptoCurrency> cryptoCurrenciesComboBox;
private Button resetDontShowAgainButton, resyncDaoFromGenesisButton, resyncDaoFromResourcesButton;
// private ListChangeListener<TradeCurrency> displayCurrenciesListChangeListener;
private ObservableList<BlockChainExplorer> blockExplorers;
private ObservableList<BlockChainExplorer> bsqBlockChainExplorers;
private ObservableList<String> languageCodes;
@ -232,7 +228,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
///////////////////////////////////////////////////////////////////////////////////////////
private void initializeGeneralOptions() {
int titledGroupBgRowSpan = displayStandbyModeFeature ? 8 : 7;
int titledGroupBgRowSpan = displayStandbyModeFeature ? 10 : 9;
TitledGroupBg titledGroupBg = addTitledGroupBg(root, gridRow, titledGroupBgRowSpan, Res.get("setting.preferences.general"));
GridPane.setColumnSpan(titledGroupBg, 1);
@ -367,6 +363,8 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
}
};
autoConfirmXmr = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.autoConfirmXmr"));
if (displayStandbyModeFeature) {
// AvoidStandbyModeService feature works only on OSX & Windows
avoidStandbyMode = addSlideToggleButton(root, ++gridRow,
@ -592,7 +590,6 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
TitledGroupBg titledGroupBg = addTitledGroupBg(root, ++gridRow, 5, Res.get("setting.preferences.displayOptions"), Layout.GROUP_DISTANCE);
GridPane.setColumnSpan(titledGroupBg, 1);
// showOwnOffersInOfferBook = addLabelCheckBox(root, gridRow, Res.get("setting.preferences.showOwnOffers"), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
showOwnOffersInOfferBook = addSlideToggleButton(root, gridRow, Res.get("setting.preferences.showOwnOffers"), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
useAnimations = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useAnimations"));
useDarkMode = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.useDarkMode"));
@ -649,18 +646,6 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
///////////////////////////////////////////////////////////////////////////////////////////
private void activateGeneralOptions() {
/* List<BaseCurrencyNetwork> baseCurrencyNetworks = Arrays.asList(BaseCurrencyNetwork.values());
// We allow switching to testnet to make it easier for users to test the testnet DAO version
// We only show mainnet and dao testnet. Testnet is rather un-usable for application testing when asics
// create 10000s of blocks per day.
baseCurrencyNetworks = baseCurrencyNetworks.stream()
.filter(e -> e.isMainnet() || e.isDaoBetaNet() || e.isDaoRegTest())
.collect(Collectors.toList());
selectBaseCurrencyNetworkComboBox.setItems(FXCollections.observableArrayList(baseCurrencyNetworks));
selectBaseCurrencyNetworkComboBox.setOnAction(e -> onSelectNetwork());
selectBaseCurrencyNetworkComboBox.getSelectionModel().select(BaseCurrencyNetwork.CURRENT_VALUE);*/
boolean useCustomWithdrawalTxFee = preferences.isUseCustomWithdrawalTxFee();
useCustomFee.setSelected(useCustomWithdrawalTxFee);
@ -705,17 +690,6 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
.show();
}
}
// Should we apply the changed currency immediately to the language list?
// If so and the user selects a unknown language he might get lost and it is hard to find
// again the language he understands
/* if (selectedItem != null && !selectedItem.equals(preferences.getUserLanguage())) {
preferences.setUserLanguage(selectedItem);
UserThread.execute(() -> {
languageCodes.clear();
languageCodes.addAll(LanguageUtil.getAllLanguageCodes());
userLanguageComboBox.getSelectionModel().select(preferences.getUserLanguage());
});
}*/
});
userCountryComboBox.setItems(countries);
@ -839,9 +813,6 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
useDarkMode.setSelected(preferences.getCssTheme() == 1);
useDarkMode.setOnAction(e -> preferences.setCssTheme(useDarkMode.isSelected()));
// useStickyMarketPriceCheckBox.setSelected(preferences.isUseStickyMarketPrice());
// useStickyMarketPriceCheckBox.setOnAction(e -> preferences.setUseStickyMarketPrice(useStickyMarketPriceCheckBox.isSelected()));
sortMarketCurrenciesNumerically.setSelected(preferences.isSortMarketCurrenciesNumerically());
sortMarketCurrenciesNumerically.setOnAction(e -> preferences.setSortMarketCurrenciesNumerically(sortMarketCurrenciesNumerically.isSelected()));
@ -855,6 +826,9 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
} else {
preferences.setUseStandbyMode(false);
}
autoConfirmXmr.setSelected(preferences.isAutoConfirmXmr());
autoConfirmXmr.setOnAction(e -> preferences.setAutoConfirmXmr(autoConfirmXmr.isSelected()));
}
private void activateDaoPreferences() {
@ -943,22 +917,6 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
blockNotifyPortTextField.setDisable(daoOptionsSet);
}
/* private void onSelectNetwork() {
if (selectBaseCurrencyNetworkComboBox.getSelectionModel().getSelectedItem() != BaseCurrencyNetwork.CURRENT_VALUE)
selectNetwork();
}
private void selectNetwork() {
new Popup().warning(Res.get("settings.net.needRestart"))
.onAction(() -> {
bisqEnvironment.saveBaseCryptoNetwork(selectBaseCurrencyNetworkComboBox.getSelectionModel().getSelectedItem());
UserThread.runAfter(BisqApp.getShutDownHandler(), 500, TimeUnit.MILLISECONDS);
})
.actionButtonText(Res.get("shared.shutDown"))
.closeButtonText(Res.get("shared.cancel"))
.onClose(() -> selectBaseCurrencyNetworkComboBox.getSelectionModel().select(BaseCurrencyNetwork.CURRENT_VALUE))
.show();
}*/
///////////////////////////////////////////////////////////////////////////////////////////
// Deactivate
@ -995,6 +953,8 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
if (displayStandbyModeFeature) {
avoidStandbyMode.setOnAction(null);
}
autoConfirmXmr.setOnAction(null);
}
private void deactivateDaoPreferences() {

View file

@ -293,6 +293,7 @@ message CounterCurrencyTransferStartedMessage {
bytes buyer_signature = 4;
string counter_currency_tx_id = 5;
string uid = 6;
string counter_currency_extra_data = 7;
}
message FinalizePayoutTxRequest {
@ -1385,6 +1386,7 @@ message Trade {
PubKeyRing refund_agent_pub_key_ring = 34;
RefundResultState refund_result_state = 35;
int64 last_refresh_request_date = 36;
string counter_currency_extra_data = 37;
}
message BuyerAsMakerTrade {
@ -1545,6 +1547,7 @@ message PreferencesPayload {
int32 block_notify_port = 53;
int32 css_theme = 54;
bool tac_accepted_v120 = 55;
bool auto_confirm_xmr = 56;
}
///////////////////////////////////////////////////////////////////////////////////////////