support broadcasting maker and taker reserve txs in legacy ui

Co-authored-by: niyid <neeyeed@gmail.com>
This commit is contained in:
woodser 2023-03-02 13:43:37 -05:00
parent 34b79e779b
commit ed0f458bc4
9 changed files with 716 additions and 79 deletions

View file

@ -232,7 +232,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
@Override
public void readPersisted(Runnable completeHandler) {
// read open offers
persistenceManager.readPersisted(persisted -> {
openOffers.setAll(persisted.getList());
@ -496,12 +496,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
checkNotNull(offer.getMakerFee(), "makerFee must not be null");
boolean autoSplit = false; // TODO: support in api
// TODO (woodser): validate offer
// create open offer
OpenOffer openOffer = new OpenOffer(offer, triggerPrice, autoSplit);
// process open offer to schedule or post
processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> {
addOpenOffer(openOffer);
@ -702,6 +702,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
}
public ObservableList<SignedOffer> getObservableSignedOffersList() {
synchronized (signedOffers) {
return signedOffers.getObservableList();
}
}
public ObservableList<OpenOffer> getObservableList() {
return openOffers.getObservableList();
}
@ -711,7 +718,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst();
}
}
public Optional<SignedOffer> getSignedOfferById(String offerId) {
synchronized (signedOffers) {
return signedOffers.stream().filter(e -> e.getOfferId().equals(offerId)).findFirst();
@ -939,11 +946,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private void handleSignOfferRequest(SignOfferRequest request, NodeAddress peer) {
log.info("Received SignOfferRequest from {} with offerId {} and uid {}",
peer, request.getOfferId(), request.getUid());
boolean result = false;
String errorMessage = null;
try {
// verify this node is an arbitrator
Arbitrator thisArbitrator = user.getRegisteredArbitrator();
NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress();
@ -953,7 +960,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify arbitrator is signer of offer payload
if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) {
errorMessage = "Cannot sign offer because offer payload is for a different arbitrator";
@ -961,7 +968,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify offer not seen before
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
if (openOfferOptional.isPresent()) {
@ -980,7 +987,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker's reserve tx (double spend, trade fee, trade amount, mining fee)
BigInteger sendAmount = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? Coin.ZERO : offer.getAmount());
BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit());
@ -999,13 +1006,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
String signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), offerPayloadAsJson);
OfferPayload signedOfferPayload = request.getOfferPayload();
signedOfferPayload.setArbitratorSignature(signature);
// create record of signed offer
SignedOffer signedOffer = new SignedOffer(
System.currentTimeMillis(),
signedOfferPayload.getId(),
offer.getAmount().longValue(),
HavenoUtils.getMakerFee(offer.getAmount()).longValue(), // TODO: these values are centineros, whereas reserve tx mining fee is BigInteger
HavenoUtils.coinToAtomicUnits(offer.getAmount()).longValueExact(),
HavenoUtils.coinToAtomicUnits(HavenoUtils.getMakerFee(offer.getAmount())).longValueExact(),
request.getReserveTxHash(),
request.getReserveTxHex(),
request.getReserveTxKeyImages(),
@ -1049,7 +1056,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage);
}
}
private void handleSignOfferResponse(SignOfferResponse response, NodeAddress peer) {
log.info("Received SignOfferResponse from {} with offerId {} and uid {}",
peer, response.getOfferId(), response.getUid());
@ -1122,7 +1129,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
Offer offer = openOffer.getOffer();
if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) {
// maker signs taker's request
String tradeRequestAsJson = JsonUtil.objectToJson(request.getTradeRequest());
makerSignature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), tradeRequestAsJson);
@ -1204,7 +1211,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private boolean apiUserDeniedByOffer(OfferAvailabilityRequest request) {
return preferences.isDenyApiTaker() && request.isTakerApiUser();
}
private boolean takerDeniedByMaker(OfferAvailabilityRequest request) {
if (request.getTradeRequest() == null) return true;
return false; // TODO (woodser): implement taker verification here, doing work of ApplyFilter and VerifyPeersAccountAgeWitness
@ -1251,7 +1258,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
///////////////////////////////////////////////////////////////////////////////////////////
// Update persisted offer if a new capability is required after a software update
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): arbitrator signature will be invalid if offer updated (exclude updateable fields from signature? re-sign?)
private void maybeUpdatePersistedOffers() {

View file

@ -17,11 +17,6 @@
package bisq.core.trade;
import bisq.common.config.Config;
import bisq.common.crypto.Hash;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.util.Utilities;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.support.dispute.arbitration.ArbitrationManager;
@ -33,11 +28,27 @@ import bisq.core.util.JsonUtil;
import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.CoinUtil;
import bisq.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j;
import bisq.common.config.Config;
import bisq.common.crypto.Hash;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.MonetaryFormat;
import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.net.URI;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@ -47,12 +58,9 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.MonetaryFormat;
import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets;
import javax.annotation.Nullable;
/**
* Collection of utilities.
@ -62,26 +70,19 @@ public class HavenoUtils {
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
public static final String LOCALHOST = "localhost";
// multipliers to convert units
public static BigInteger CENTINEROS_AU_MULTIPLIER = new BigInteger("10000");
private static BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000");
// global thread pool
public static final BigInteger CENTINEROS_AU_MULTIPLIER = new BigInteger("10000");
private static final BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000");
public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("0.000000000000");
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
private static final int POOL_SIZE = 10;
private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE);
// TODO: better way to share reference?
public static ArbitrationManager arbitrationManager;
public static ArbitrationManager arbitrationManager; // TODO: better way to share reference?
public static BigInteger coinToAtomicUnits(Coin coin) {
return centinerosToAtomicUnits(coin.value);
}
public static double coinToXmr(Coin coin) {
return atomicUnitsToXmr(coinToAtomicUnits(coin));
}
public static BigInteger centinerosToAtomicUnits(long centineros) {
return BigInteger.valueOf(centineros).multiply(CENTINEROS_AU_MULTIPLIER);
}
@ -102,10 +103,18 @@ public class HavenoUtils {
return atomicUnits.divide(CENTINEROS_AU_MULTIPLIER).longValueExact();
}
public static Coin atomicUnitsToCoin(BigInteger atomicUnits) {
public static Coin atomicUnitsToCoin(long atomicUnits) {
return Coin.valueOf(atomicUnitsToCentineros(atomicUnits));
}
public static Coin atomicUnitsToCoin(BigInteger atomicUnits) {
return atomicUnitsToCoin(atomicUnits.longValueExact());
}
public static double atomicUnitsToXmr(long atomicUnits) {
return atomicUnitsToXmr(BigInteger.valueOf(atomicUnits));
}
public static double atomicUnitsToXmr(BigInteger atomicUnits) {
return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue();
}
@ -117,7 +126,27 @@ public class HavenoUtils {
public static long xmrToCentineros(double xmr) {
return atomicUnitsToCentineros(xmrToAtomicUnits(xmr));
}
public static double coinToXmr(Coin coin) {
return atomicUnitsToXmr(coinToAtomicUnits(coin));
}
public static String formatXmrWithCode(Coin coin) {
return formatXmrWithCode(coinToAtomicUnits(coin).longValueExact());
}
public static String formatXmrWithCode(long atomicUnits) {
String formatted = XMR_FORMATTER.format(atomicUnitsToXmr(atomicUnits));
// strip trailing 0s
if (formatted.contains(".")) {
while (formatted.length() > 3 && formatted.charAt(formatted.length() - 1) == '0') {
formatted = formatted.substring(0, formatted.length() - 1);
}
}
return formatted.concat(" ").concat("XMR");
}
private static final MonetaryFormat xmrCoinFormat = Config.baseCurrencyNetworkParameters().getMonetaryFormat();
@Nullable
@ -166,7 +195,7 @@ public class HavenoUtils {
/**
* Get address to collect trade fees.
*
*
* @return the address which collects trade fees
*/
public static String getTradeFeeAddress() {
@ -196,7 +225,7 @@ public class HavenoUtils {
/**
* Returns a unique deterministic id for sending a trade mailbox message.
*
*
* @param trade the trade
* @param tradeMessageClass the trade message class
* @param receiver the receiver address
@ -209,23 +238,23 @@ public class HavenoUtils {
/**
* Check if the arbitrator signature is valid for an offer.
*
*
* @param offer is a signed offer with payload
* @param arbitrator is the original signing arbitrator
* @return true if the arbitrator's signature is valid for the offer
*/
public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) {
// copy offer payload
OfferPayload offerPayloadCopy = OfferPayload.fromProto(offer.toProtoMessage().getOfferPayload());
// remove arbitrator signature from signed payload
String signature = offerPayloadCopy.getArbitratorSignature();
offerPayloadCopy.setArbitratorSignature(null);
// get unsigned offer payload as json string
String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy);
// verify arbitrator signature
try {
return Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature);
@ -233,15 +262,15 @@ public class HavenoUtils {
return false;
}
}
/**
* Check if the maker signature for a trade request is valid.
*
*
* @param request is the trade request to check
* @return true if the maker's signature is valid for the trade request
*/
public static boolean isMakerSignatureValid(InitTradeRequest request, String signature, PubKeyRing makerPubKeyRing) {
// re-create trade request with signed fields
InitTradeRequest signedRequest = new InitTradeRequest(
request.getTradeId(),
@ -266,10 +295,10 @@ public class HavenoUtils {
request.getPayoutAddress(),
null
);
// get trade request as string
String tradeRequestAsJson = JsonUtil.objectToJson(signedRequest);
// verify maker signature
try {
return Sig.verify(makerPubKeyRing.getSignaturePubKey(),
@ -282,7 +311,7 @@ public class HavenoUtils {
/**
* Verify the buyer signature for a PaymentSentMessage.
*
*
* @param trade - the trade to verify
* @param message - signed payment sent message to verify
* @return true if the buyer's signature is valid for the message
@ -298,7 +327,7 @@ public class HavenoUtils {
// replace signature
message.setBuyerSignature(signature);
// verify signature
String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
try {
@ -313,7 +342,7 @@ public class HavenoUtils {
/**
* Verify the seller signature for a PaymentReceivedMessage.
*
*
* @param trade - the trade to verify
* @param message - signed payment received message to verify
* @return true if the seller's signature is valid for the message
@ -329,7 +358,7 @@ public class HavenoUtils {
// replace signature
message.setSellerSignature(signature);
// verify signature
String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId();
try {

View file

@ -226,6 +226,8 @@ shared.numItemsLabel=Number of entries: {0}
shared.filter=Filter
shared.enabled=Enabled
shared.me=Me
shared.maker=Maker
shared.taker=Taker
####################################################################
@ -990,7 +992,11 @@ portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at th
Try again after completion of trade(s) {0}
portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null.
portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null.
portfolio.failed.penalty.msg=This will charge the {0}/{1} the trade fee of {2} and return the remaining trade funds to their wallet. Are you sure you want to send?\n\n\
Other Info:\n\
Transaction Fee: {3}\n\
Reserve Tx Hash: {4}
portfolio.failed.error.msg=Trade record does not exist.
####################################################################
# Funds
@ -1089,6 +1095,18 @@ support.tab.legacyArbitration.support=Legacy Arbitration
support.tab.ArbitratorsSupportTickets={0}'s tickets
support.filter=Search disputes
support.filter.prompt=Enter trade ID, date, onion address or account data
support.tab.SignedOffers=Signed Offers
support.prompt.signedOffer.penalty.msg=This will charge the maker the trade fee and return their remaining trade funds to their wallet. Are you sure you want to send?\n\n\
Offer ID: {0}\n\
Maker Trade Fee: {1}\n\
Reserve Tx Miner Fee: {2}\n\
Reserve Tx Hash: {3}\n\
Reserve Tx Key Images: {4}\n\
support.contextmenu.penalize.msg=Penalize {0} by publishing reserve tx
support.prompt.signedOffer.error.msg=Signed Offer record does not exist; contact administrator.
support.info.submitTxHex=Reserve transaction has been published with the following result:\n
support.result.success=Transaction hex has been successfully submitted.
support.sigCheck.button=Check signature
support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the \
@ -1148,6 +1166,12 @@ support.buyerMaker=XMR buyer/Maker
support.sellerMaker=XMR seller/Maker
support.buyerTaker=XMR buyer/Taker
support.sellerTaker=XMR seller/Taker
support.txKeyImages=Key Images
support.txHash=Transaction Hash
support.txHex=Transaction Hex
support.signature=Signature
support.maker.trade.fee=Maker Trade Fee
support.tx.miner.fee=Miner Fee
support.backgroundInfo=Haveno is not a company, so it handles disputes differently.\n\n\
Traders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. \

View file

@ -21,7 +21,6 @@ import bisq.desktop.Navigation;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.main.MainView;
import bisq.desktop.main.offer.offerbook.BtcOfferBookView;
import bisq.desktop.main.offer.offerbook.OfferBookView;
import bisq.desktop.main.offer.offerbook.OtherOfferBookView;
@ -29,6 +28,7 @@ import bisq.desktop.main.offer.offerbook.TopAltcoinOfferBookView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.GUIUtil;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.CryptoCurrency;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
@ -40,6 +40,7 @@ import bisq.common.UserThread;
import bisq.common.util.Tuple2;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
@ -54,9 +55,16 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import monero.daemon.model.MoneroSubmitTxResult;
// Shared utils for Views
@Slf4j
public class OfferViewUtil {
public static Label createPopOverLabel(String text) {
@ -166,4 +174,18 @@ public class OfferViewUtil {
return CurrencyUtil.getMainCryptoCurrencies().stream().filter(cryptoCurrency ->
!Objects.equals(cryptoCurrency.getCode(), GUIUtil.TOP_ALTCOIN.getCode()));
}
public static void submitTransactionHex(XmrWalletService xmrWalletService,
TableView tableView,
String reserveTxHex) {
MoneroSubmitTxResult result = xmrWalletService.getDaemon().submitTxHex(reserveTxHex);
log.info("submitTransactionHex: reserveTxHex={} result={}", result);
tableView.refresh();
if(result.isGood()) {
new Popup().information(Res.get("support.result.success")).show();
} else {
new Popup().attention(result.toString()).show();
}
}
}

View file

@ -23,23 +23,26 @@ import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.offer.OfferViewUtil;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.trade.Contract;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade;
import bisq.common.config.Config;
import bisq.common.util.Utilities;
import org.bitcoinj.core.Coin;
import com.googlecode.jcsv.writer.CSVEntryConverter;
import javax.inject.Inject;
import javax.inject.Named;
import de.jensd.fx.fontawesome.AwesomeIcon;
@ -50,9 +53,12 @@ import javafx.fxml.FXML;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
@ -107,12 +113,16 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
private EventHandler<KeyEvent> keyEventEventHandler;
private ChangeListener<String> filterTextFieldListener;
private Scene scene;
private XmrWalletService xmrWalletService;
private ContextMenu contextMenu;
@Inject
public FailedTradesView(FailedTradesViewModel model,
TradeDetailsWindow tradeDetailsWindow) {
TradeDetailsWindow tradeDetailsWindow,
XmrWalletService xmrWalletService) {
super(model);
this.tradeDetailsWindow = tradeDetailsWindow;
this.xmrWalletService = xmrWalletService;
}
@Override
@ -195,6 +205,39 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedList);
contextMenu = new ContextMenu();
MenuItem item1 = new MenuItem(Res.get("support.contextmenu.penalize.msg", Res.get("shared.maker")));
MenuItem item2 = new MenuItem(Res.get("support.contextmenu.penalize.msg", Res.get("shared.taker")));
contextMenu.getItems().addAll(item1, item2);
tableView.setRowFactory(tv -> {
TableRow<FailedTradesListItem> row = new TableRow<>();
row.setOnContextMenuRequested(event -> {
contextMenu.show(row, event.getScreenX(), event.getScreenY());
});
return row;
});
item1.setOnAction(event -> {
Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade();
handleContextMenu("portfolio.failed.penalty.msg",
Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"),
Res.get("shared.maker"),
selectedFailedTrade.getMakerFee(),
selectedFailedTrade.getMaker().getReserveTxHash(),
selectedFailedTrade.getMaker().getReserveTxHex());
});
item2.setOnAction(event -> {
Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade();
handleContextMenu("portfolio.failed.penalty.msg",
Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"),
Res.get("shared.taker"),
selectedFailedTrade.getTakerFee(),
selectedFailedTrade.getTaker().getReserveTxHash(),
selectedFailedTrade.getTaker().getReserveTxHex());
});
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
exportButton.setOnAction(event -> {
ObservableList<TableColumn<FailedTradesListItem, ?>> tableColumns = tableView.getColumns();
@ -230,6 +273,23 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
applyFilteredListPredicate(filterTextField.getText());
}
private void handleContextMenu(String msgKey, String buyerOrSeller, String makerOrTaker, Coin fee, String reserveTxHash, String reserveTxHex) {
final Trade failedTrade = tableView.getSelectionModel().getSelectedItem().getTrade();
log.debug("Found {} matching trade.", (failedTrade != null ? failedTrade.getId() : null));
if(failedTrade != null) {
new Popup().warning(Res.get(msgKey,
buyerOrSeller,
makerOrTaker,
HavenoUtils.formatXmrWithCode(fee),
"todo", // TODO: set reserve tx miner fee when verified
reserveTxHash
)
).onAction(() -> OfferViewUtil.submitTransactionHex(xmrWalletService, tableView, reserveTxHex)).show();
} else {
new Popup().error(Res.get("portfolio.failed.error.msg")).show();
}
}
@Override
protected void deactivate() {
if (scene != null) {

View file

@ -22,14 +22,11 @@ import bisq.desktop.common.view.ActivatableViewAndModel;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.components.PeerInfoIcon;
import bisq.desktop.components.PeerInfoIconTrading;
import bisq.desktop.components.list.FilterBox;
import bisq.desktop.main.MainView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.main.portfolio.PortfolioView;
import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
import bisq.desktop.main.portfolio.presentation.PortfolioUtil;
import bisq.desktop.main.shared.ChatView;
import bisq.desktop.util.CssTheme;
@ -55,7 +52,6 @@ import bisq.network.p2p.NodeAddress;
import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import bisq.common.util.Utilities;
import javax.inject.Inject;

View file

@ -25,6 +25,7 @@ import bisq.desktop.common.view.View;
import bisq.desktop.common.view.ViewLoader;
import bisq.desktop.main.MainView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.support.dispute.agent.SignedOfferView;
import bisq.desktop.main.support.dispute.agent.arbitration.ArbitratorView;
import bisq.desktop.main.support.dispute.agent.mediation.MediatorView;
import bisq.desktop.main.support.dispute.agent.refund.RefundAgentView;
@ -69,7 +70,8 @@ public class SupportView extends ActivatableView<TabPane, Void> {
private Tab mediatorTab, refundAgentTab;
@Nullable
private Tab arbitratorTab;
@Nullable
private Tab signedOfferTab;
private final Navigation navigation;
private final ArbitratorManager arbitratorManager;
private final MediatorManager mediatorManager;
@ -77,6 +79,7 @@ public class SupportView extends ActivatableView<TabPane, Void> {
private final ArbitrationManager arbitrationManager;
private final MediationManager mediationManager;
private final RefundManager refundManager;
private final KeyRing keyRing;
private Navigation.Listener navigationListener;
@ -143,6 +146,8 @@ public class SupportView extends ActivatableView<TabPane, Void> {
navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class);
else if (newValue == arbitratorTab)
navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class);
else if (newValue == signedOfferTab)
navigation.navigateTo(MainView.class, SupportView.class, SignedOfferView.class);
else if (newValue == mediatorTab)
navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class);
else if (newValue == refundAgentTab)
@ -161,15 +166,14 @@ public class SupportView extends ActivatableView<TabPane, Void> {
if (hasArbitrationCases) {
boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream()
.anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing));
if (arbitratorTab == null) {
// In case a arbitrator has become inactive he still might get disputes from pending trades
boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream()
.anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing));
if (isActiveArbitrator || hasDisputesAsArbitrator) {
arbitratorTab = new Tab();
arbitratorTab.setClosable(false);
root.getTabs().add(arbitratorTab);
}
// In case a arbitrator has become inactive he still might get disputes from pending trades
boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream()
.anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing));
if (arbitratorTab == null && (isActiveArbitrator || hasDisputesAsArbitrator)) {
arbitratorTab = new Tab();
arbitratorTab.setClosable(false);
root.getTabs().add(arbitratorTab);
}
}
@ -186,6 +190,12 @@ public class SupportView extends ActivatableView<TabPane, Void> {
}
}
if (signedOfferTab == null) {
signedOfferTab = new Tab();
signedOfferTab.setClosable(false);
root.getTabs().add(signedOfferTab);
}
boolean isActiveRefundAgent = refundAgentManager.getObservableMap().values().stream()
.anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing));
if (refundAgentTab == null) {
@ -203,6 +213,9 @@ public class SupportView extends ActivatableView<TabPane, Void> {
if (arbitratorTab != null) {
arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator")).toUpperCase());
}
if (signedOfferTab != null) {
signedOfferTab.setText(Res.get("support.tab.SignedOffers").toUpperCase());
}
if (mediatorTab != null) {
mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator")).toUpperCase());
}
@ -235,6 +248,8 @@ public class SupportView extends ActivatableView<TabPane, Void> {
navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class);
} else if (arbitratorTab != null) {
navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class);
} else if (signedOfferTab != null) {
navigation.navigateTo(MainView.class, SupportView.class, SignedOfferView.class);
} else if (mediatorTab != null) {
navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class);
} else if (refundAgentTab != null) {
@ -275,6 +290,8 @@ public class SupportView extends ActivatableView<TabPane, Void> {
currentTab = tradersRefundDisputesTab;
} else if (view instanceof ArbitratorView) {
currentTab = arbitratorTab;
} else if (view instanceof SignedOfferView) {
currentTab = signedOfferTab;
} else if (view instanceof MediatorView) {
currentTab = mediatorTab;
} else if (view instanceof RefundAgentView) {

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<?import javafx.geometry.Insets?>
<VBox fx:id="root" fx:controller="bisq.desktop.main.support.dispute.agent.SignedOfferView"
spacing="10" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0"/>
</padding>
<TableView fx:id="tableView" VBox.vgrow="ALWAYS" />
<HBox spacing="10">
<Label fx:id="numItems"/>
<Region fx:id="footerSpacer"/>
</HBox>
</VBox>

View file

@ -0,0 +1,444 @@
/*
* 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 bisq.desktop.main.support.dispute.agent;
import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.offer.OfferViewUtil;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.common.UserThread;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.Res;
import bisq.core.offer.OpenOfferManager;
import bisq.core.offer.SignedOffer;
import bisq.core.trade.HavenoUtils;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.geometry.Insets;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.SortedList;
import javafx.util.Callback;
import javafx.util.Duration;
import java.util.Comparator;
import java.util.Date;
@FxmlView
public class SignedOfferView extends ActivatableView<VBox, Void> {
private final OpenOfferManager openOfferManager;
@FXML
protected TableView<SignedOffer> tableView;
@FXML
TableColumn<SignedOffer, SignedOffer> dateColumn;
@FXML
TableColumn<SignedOffer, SignedOffer> offerIdColumn;
@FXML
TableColumn<SignedOffer, SignedOffer> reserveTxHashColumn;
@FXML
TableColumn<SignedOffer, SignedOffer> reserveTxHexColumn;
@FXML
TableColumn<SignedOffer, SignedOffer> reserveTxKeyImages;
@FXML
TableColumn<SignedOffer, SignedOffer> arbitratorSignatureColumn;
@FXML
TableColumn<SignedOffer, SignedOffer> reserveTxMinerFeeColumn;
@FXML
TableColumn<SignedOffer, SignedOffer> makerTradeFeeColumn;
@FXML
InputTextField filterTextField;
@FXML
Label numItems;
@FXML
Region footerSpacer;
private SignedOffer selectedSignedOffer;
private XmrWalletService xmrWalletService;
private ContextMenu contextMenu;
private final ListChangeListener<SignedOffer> signedOfferListChangeListener;
@Inject
public SignedOfferView(OpenOfferManager openOfferManager, XmrWalletService xmrWalletService) {
this.openOfferManager = openOfferManager;
this.xmrWalletService = xmrWalletService;
signedOfferListChangeListener = change -> applyList();
}
private void applyList() {
UserThread.execute(() -> {
SortedList<SignedOffer> sortedList = new SortedList<>(openOfferManager.getObservableSignedOffersList());
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedList);
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Life cycle
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void initialize() {
Label label = new AutoTooltipLabel(Res.get("support.filter"));
HBox.setMargin(label, new Insets(5, 0, 0, 0));
HBox.setHgrow(label, Priority.NEVER);
filterTextField = new InputTextField();
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(100));
tooltip.setShowDuration(Duration.seconds(10));
filterTextField.setTooltip(tooltip);
HBox.setHgrow(filterTextField, Priority.NEVER);
filterTextField.setText("open");
setupTable();
}
@Override
protected void activate() {
super.activate();
applyList();
openOfferManager.getObservableSignedOffersList().addListener(signedOfferListChangeListener);
contextMenu = new ContextMenu();
MenuItem item1 = new MenuItem(Res.get("support.contextmenu.penalize.msg",
Res.get("shared.maker")));
contextMenu.getItems().addAll(item1);
tableView.setRowFactory(tv -> {
TableRow<SignedOffer> row = new TableRow<>();
row.setOnContextMenuRequested(event -> {
contextMenu.show(row, event.getScreenX(), event.getScreenY());
});
return row;
});
item1.setOnAction(event -> {
selectedSignedOffer = tableView.getSelectionModel().getSelectedItem();
if(selectedSignedOffer != null) {
new Popup().warning(Res.get("support.prompt.signedOffer.penalty.msg",
selectedSignedOffer.getOfferId(),
HavenoUtils.formatXmrWithCode(selectedSignedOffer.getMakerTradeFee()),
HavenoUtils.formatXmrWithCode(selectedSignedOffer.getReserveTxMinerFee()),
selectedSignedOffer.getReserveTxHash(),
selectedSignedOffer.getReserveTxKeyImages())
).onAction(() -> OfferViewUtil.submitTransactionHex(xmrWalletService, tableView,
selectedSignedOffer.getReserveTxHex())).show();
} else {
new Popup().error(Res.get("support.prompt.signedOffer.error.msg")).show();
}
});
GUIUtil.requestFocus(tableView);
}
@Override
protected void deactivate() {
super.deactivate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// SignedOfferView
///////////////////////////////////////////////////////////////////////////////////////////
protected void setupTable() {
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets"));
placeholder.setWrapText(true);
tableView.setPlaceholder(placeholder);
tableView.getSelectionModel().clearSelection();
dateColumn = getDateColumn();
tableView.getColumns().add(dateColumn);
offerIdColumn = getOfferIdColumn();
tableView.getColumns().add(offerIdColumn);
reserveTxHashColumn = getReserveTxHashColumn();
tableView.getColumns().add(reserveTxHashColumn);
reserveTxHexColumn = getReserveTxHexColumn();
tableView.getColumns().add(reserveTxHexColumn);
reserveTxKeyImages = getReserveTxKeyImagesColumn();
tableView.getColumns().add(reserveTxKeyImages);
arbitratorSignatureColumn = getArbitratorSignatureColumn();
tableView.getColumns().add(arbitratorSignatureColumn);
makerTradeFeeColumn = getMakerTradeFeeColumn();
tableView.getColumns().add(makerTradeFeeColumn);
reserveTxMinerFeeColumn = getReserveTxMinerFeeColumn();
tableView.getColumns().add(reserveTxMinerFeeColumn);
offerIdColumn.setComparator(Comparator.comparing(SignedOffer::getOfferId));
dateColumn.setComparator(Comparator.comparing(SignedOffer::getTimeStamp));
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
tableView.getSortOrder().add(dateColumn);
}
private TableColumn<SignedOffer, SignedOffer> getDateColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("shared.date")) {
{
setMinWidth(180);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(DisplayUtils.formatDateTime(new Date(item.getTimeStamp())));
else
setText("");
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getOfferIdColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("shared.offerId")) {
{
setMinWidth(110);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
private HyperlinkWithIcon field;
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(item.getOfferId());
setGraphic(field);
} else {
setGraphic(null);
setText("");
if (field != null)
field.setOnAction(null);
}
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getReserveTxHashColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("support.txHash")) {
{
setMinWidth(160);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(item.getReserveTxHash());
else
setText("");
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getReserveTxHexColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("support.txHex")) {
{
setMinWidth(160);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(item.getReserveTxHex());
else
setText("");
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getReserveTxKeyImagesColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("support.txKeyImages")) {
{
setMinWidth(160);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(item.getReserveTxKeyImages().toString());
else
setText("");
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getArbitratorSignatureColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("support.signature")) {
{
setMinWidth(160);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(item.getArbitratorSignature());
else
setText("");
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getMakerTradeFeeColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("support.maker.trade.fee")) {
{
setMinWidth(160);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(HavenoUtils.formatXmrWithCode(item.getMakerTradeFee()));
else
setText("");
}
};
}
});
return column;
}
private TableColumn<SignedOffer, SignedOffer> getReserveTxMinerFeeColumn() {
TableColumn<SignedOffer, SignedOffer> column = new AutoTooltipTableColumn<>(Res.get("support.tx.miner.fee")) {
{
setMinWidth(160);
}
};
column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<SignedOffer, SignedOffer> call(TableColumn<SignedOffer, SignedOffer> column) {
return new TableCell<>() {
@Override
public void updateItem(final SignedOffer item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty)
setText(HavenoUtils.formatXmrWithCode(item.getReserveTxMinerFee()));
else
setText("");
}
};
}
});
return column;
}
}