diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 18459682..e519be1b 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -100,6 +100,7 @@ public class OfferBookService { connectionsService.addListener(new MoneroConnectionManagerListener() { @Override public void onConnectionChanged(MoneroRpcConnection connection) { + if (keyImagePoller == null) return; keyImagePoller.setDaemon(connectionsService.getDaemon()); keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 1d9d2bec..dd8f814a 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -925,7 +925,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe signedOfferPayload.setArbitratorSignature(signature); // create record of signed offer - SignedOffer signedOffer = new SignedOffer(signedOfferPayload.getId(), request.getReserveTxHash(), request.getReserveTxHex(), signature); // TODO (woodser): no need for signature to be part of SignedOffer? + SignedOffer signedOffer = new SignedOffer( + System.currentTimeMillis(), + signedOfferPayload.getId(), + request.getReserveTxHash(), + request.getReserveTxHex(), + request.getReserveTxKeyImages(), + signature); // TODO (woodser): no need for signature to be part of SignedOffer? addSignedOffer(signedOffer); requestPersistence(); diff --git a/core/src/main/java/bisq/core/offer/SignedOffer.java b/core/src/main/java/bisq/core/offer/SignedOffer.java index 026d16ec..fc679798 100644 --- a/core/src/main/java/bisq/core/offer/SignedOffer.java +++ b/core/src/main/java/bisq/core/offer/SignedOffer.java @@ -17,6 +17,8 @@ package bisq.core.offer; +import java.util.List; + import bisq.common.proto.persistable.PersistablePayload; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -26,6 +28,8 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public final class SignedOffer implements PersistablePayload { + @Getter + private final long timeStamp; @Getter private final String offerId; @Getter @@ -33,12 +37,16 @@ public final class SignedOffer implements PersistablePayload { @Getter private final String reserveTxHex; @Getter + private final List reserveTxKeyImages; + @Getter private final String arbitratorSignature; - public SignedOffer(String offerId, String reserveTxHash, String reserveTxHex, String arbitratorSignature) { + public SignedOffer(long timeStamp, String offerId, String reserveTxHash, String reserveTxHex, List reserveTxKeyImages, String arbitratorSignature) { + this.timeStamp = timeStamp; this.offerId = offerId; this.reserveTxHash = reserveTxHash; this.reserveTxHex = reserveTxHex; + this.reserveTxKeyImages = reserveTxKeyImages; this.arbitratorSignature = arbitratorSignature; } @@ -49,16 +57,17 @@ public final class SignedOffer implements PersistablePayload { @Override public protobuf.SignedOffer toProtoMessage() { protobuf.SignedOffer.Builder builder = protobuf.SignedOffer.newBuilder() + .setTimeStamp(timeStamp) .setOfferId(offerId) .setReserveTxHash(reserveTxHash) .setReserveTxHex(reserveTxHex) + .addAllReserveTxKeyImages(reserveTxKeyImages) .setArbitratorSignature(arbitratorSignature); - return builder.build(); } public static SignedOffer fromProto(protobuf.SignedOffer proto) { - return new SignedOffer(proto.getOfferId(), proto.getReserveTxHash(), proto.getReserveTxHex(), proto.getArbitratorSignature()); + return new SignedOffer(proto.getTimeStamp(), proto.getOfferId(), proto.getReserveTxHash(), proto.getReserveTxHex(), proto.getReserveTxKeyImagesList(), proto.getArbitratorSignature()); } @@ -69,9 +78,11 @@ public final class SignedOffer implements PersistablePayload { @Override public String toString() { return "SignedOffer{" + + ",\n timeStamp=" + timeStamp + ",\n offerId=" + offerId + ",\n reserveTxHash=" + reserveTxHash + ",\n reserveTxHex=" + reserveTxHex + + ",\n reserveTxKeyImages=" + reserveTxKeyImages + ",\n arbitratorSignature=" + arbitratorSignature + "\n}"; } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 6b4f5782..1c0fbad3 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -487,7 +487,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); - removeTradeOnError(trade); + if (trade.getMaker().getReserveTxHash() != null || trade.getTaker().getReserveTxHash() != null) { + onMoveInvalidTradeToFailedTrades(trade); // arbitrator retains failed trades for analysis and penalty + } else { + removeTradeOnError(trade); + } if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java index e9586ced..47b8bc11 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java @@ -89,28 +89,32 @@ public class OfferBook { // Use offer.equals(offer) to see if the OfferBook list contains an exact // match -- offer.equals(offer) includes comparisons of payload, state // and errorMessage. - boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); - if (!hasSameOffer) { - OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); - removeDuplicateItem(newOfferBookListItem); - offerBookListItems.add(newOfferBookListItem); // Add replacement. - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("onAdded: Added new offer {}\n" - + "\twith newItem.payloadHash: {}", - offer.getId(), - newOfferBookListItem.hashOfPayload.getHex()); + synchronized (offerBookListItems) { + boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer)); + if (!hasSameOffer) { + OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer); + removeDuplicateItem(newOfferBookListItem); + offerBookListItems.add(newOfferBookListItem); // Add replacement. + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onAdded: Added new offer {}\n" + + "\twith newItem.payloadHash: {}", + offer.getId(), + newOfferBookListItem.hashOfPayload.getHex()); + } + } else { + log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); } - } else { - log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId()); + printOfferBookListItems("After onAdded"); } - printOfferBookListItems("After onAdded"); } @Override public void onRemoved(Offer offer) { - printOfferBookListItems("Before onRemoved"); - removeOffer(offer); - printOfferBookListItems("After onRemoved"); + synchronized (offerBookListItems) { + printOfferBookListItems("Before onRemoved"); + removeOffer(offer); + printOfferBookListItems("After onRemoved"); + } } }); @@ -122,77 +126,83 @@ public class OfferBook { } private void removeDuplicateItem(OfferBookListItem newOfferBookListItem) { - String offerId = newOfferBookListItem.getOffer().getId(); - // We need to remove any view items with a matching offerId before - // a newOfferBookListItem is added to the view. - List duplicateItems = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offerId)) - .collect(Collectors.toList()); - duplicateItems.forEach(oldOfferItem -> { - offerBookListItems.remove(oldOfferItem); - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("onAdded: Removed old offer {}\n" - + "\twith payload hash {} from list.\n" - + "\tThis may make a subsequent onRemoved( {} ) call redundant.", - offerId, - oldOfferItem.getHashOfPayload().getHex(), - oldOfferItem.getOffer().getId()); - } - }); + synchronized (offerBookListItems) { + String offerId = newOfferBookListItem.getOffer().getId(); + + // We need to remove any view items with a matching offerId before + // a newOfferBookListItem is added to the view. + List duplicateItems = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offerId)) + .collect(Collectors.toList()); + duplicateItems.forEach(oldOfferItem -> { + offerBookListItems.remove(oldOfferItem); + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("onAdded: Removed old offer {}\n" + + "\twith payload hash {} from list.\n" + + "\tThis may make a subsequent onRemoved( {} ) call redundant.", + offerId, + oldOfferItem.getHashOfPayload().getHex(), + oldOfferItem.getOffer().getId()); + } + }); + } } public void removeOffer(Offer offer) { - // Update state in case that that offer is used in the take offer screen, so it gets updated correctly - offer.setState(Offer.State.REMOVED); - offer.cancelAvailabilityRequest(); + synchronized (offerBookListItems) { - P2PDataStorage.ByteArray hashOfPayload = new P2PDataStorage.ByteArray(offer.getOfferPayload().getHash()); + // Update state in case that that offer is used in the take offer screen, so it gets updated correctly + offer.setState(Offer.State.REMOVED); + offer.cancelAvailabilityRequest(); - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("onRemoved: id = {}\n" - + "\twith payload-hash = {}", - offer.getId(), - hashOfPayload.getHex()); - } + P2PDataStorage.ByteArray hashOfPayload = new P2PDataStorage.ByteArray(offer.getOfferPayload().getHash()); - // Find the removal candidate in the OfferBook list with matching offerId and payload-hash. - Optional candidateWithMatchingPayloadHash = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId()) - && item.hashOfPayload.equals(hashOfPayload)) - .findAny(); - - if (!candidateWithMatchingPayloadHash.isPresent()) { if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("UI view list does not contain offer with id {} and payload-hash {}", + log.debug("onRemoved: id = {}\n" + + "\twith payload-hash = {}", offer.getId(), hashOfPayload.getHex()); } - return; - } - OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); - // Remove the candidate only if the candidate's offer payload the hash matches the - // onRemoved hashOfPayload parameter. We may receive add/remove messages out of - // order from the API's 'editoffer' method, and use the offer payload hash to - // ensure we do not remove an edited offer immediately after it was added. - if (candidate.getHashOfPayload().equals(hashOfPayload)) { - // The payload-hash test passed, remove the candidate and print reason. - offerBookListItems.remove(candidate); + // Find the removal candidate in the OfferBook list with matching offerId and payload-hash. + Optional candidateWithMatchingPayloadHash = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offer.getId()) + && item.hashOfPayload.equals(hashOfPayload)) + .findAny(); - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" - + " Yes, removed old offer", - candidate.hashOfPayload.getHex(), - hashOfPayload.getHex()); + if (!candidateWithMatchingPayloadHash.isPresent()) { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("UI view list does not contain offer with id {} and payload-hash {}", + offer.getId(), + hashOfPayload.getHex()); + } + return; } - } else { - if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. - // Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash. - // Print reason for not removing candidate. - log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" - + " No, old offer not removed", - candidate.hashOfPayload.getHex(), - hashOfPayload.getHex()); + + OfferBookListItem candidate = candidateWithMatchingPayloadHash.get(); + // Remove the candidate only if the candidate's offer payload the hash matches the + // onRemoved hashOfPayload parameter. We may receive add/remove messages out of + // order from the API's 'editoffer' method, and use the offer payload hash to + // ensure we do not remove an edited offer immediately after it was added. + if (candidate.getHashOfPayload().equals(hashOfPayload)) { + // The payload-hash test passed, remove the candidate and print reason. + offerBookListItems.remove(candidate); + + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + + " Yes, removed old offer", + candidate.hashOfPayload.getHex(), + hashOfPayload.getHex()); + } + } else { + if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR. + // Candidate's payload-hash test failed: payload-hash != onRemoved.payload-hash. + // Print reason for not removing candidate. + log.debug("Candidate.payload-hash: {} == onRemoved.payload-hash: {} ?" + + " No, old offer not removed", + candidate.hashOfPayload.getHex(), + hashOfPayload.getHex()); + } } } } @@ -202,33 +212,37 @@ public class OfferBook { } public void fillOfferBookListItems() { - try { - // setAll causes sometimes an UnsupportedOperationException - // Investigate why.... - offerBookListItems.clear(); - offerBookListItems.addAll(offerBookService.getOffers().stream() - .filter(this::isOfferAllowed) - .map(OfferBookListItem::new) - .collect(Collectors.toList())); - - log.debug("offerBookListItems.size {}", offerBookListItems.size()); - fillOfferCountMaps(); - } catch (Throwable t) { - log.error("Error at fillOfferBookListItems: " + t); + synchronized (offerBookListItems) { + try { + // setAll causes sometimes an UnsupportedOperationException + // Investigate why.... + offerBookListItems.clear(); + offerBookListItems.addAll(offerBookService.getOffers().stream() + .filter(this::isOfferAllowed) + .map(OfferBookListItem::new) + .collect(Collectors.toList())); + + log.debug("offerBookListItems.size {}", offerBookListItems.size()); + fillOfferCountMaps(); + } catch (Throwable t) { + log.error("Error at fillOfferBookListItems: " + t); + } } } public void printOfferBookListItems(String msg) { - if (log.isDebugEnabled()) { - if (offerBookListItems.size() == 0) { - log.debug("{} -> OfferBookListItems: none", msg); - return; + synchronized (offerBookListItems) { + if (log.isDebugEnabled()) { + if (offerBookListItems.size() == 0) { + log.debug("{} -> OfferBookListItems: none", msg); + return; + } + + StringBuilder stringBuilder = new StringBuilder(msg + " -> ").append("OfferBookListItems:").append("\n"); + offerBookListItems.forEach(i -> stringBuilder.append("\t").append(i.toString()).append("\n")); + stringBuilder.deleteCharAt(stringBuilder.length() - 1); + log.debug(stringBuilder.toString()); } - - StringBuilder stringBuilder = new StringBuilder(msg + " -> ").append("OfferBookListItems:").append("\n"); - offerBookListItems.forEach(i -> stringBuilder.append("\t").append(i.toString()).append("\n")); - stringBuilder.deleteCharAt(stringBuilder.length() - 1); - log.debug(stringBuilder.toString()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java index e07aa9be..7b66eb4c 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -319,63 +319,66 @@ public class TradeDetailsWindow extends Overlay { String sellerWitnessHash = trade.getSeller().getAccountAgeWitness() == null ? "null" : Utilities.bytesAsHexString(trade.getSeller().getAccountAgeWitness().getHash()); String sellerPubKeyRingHash = Utilities.bytesAsHexString(trade.getSeller().getPubKeyRing().getSignaturePubKeyBytes()); - if (contract != null) { - viewContractButton.setOnAction(e -> { - TextArea textArea = new HavenoTextArea(); - textArea.setText(trade.getContractAsJson()); - String data = "Contract as json:\n"; - data += trade.getContractAsJson(); - data += "\n\nOther detail data:"; - if (offer.isFiatOffer()) { - data += "\n\nBuyers witness hash,pub key ring hash: " + buyerWitnessHash + "," + buyerPubKeyRingHash; - data += "\nBuyers account age: " + buyersAccountAge; - data += "\nSellers witness hash,pub key ring hash: " + sellerWitnessHash + "," + sellerPubKeyRingHash; - data += "\nSellers account age: " + sellersAccountAge; - } + viewContractButton.setOnAction(e -> { + TextArea textArea = new HavenoTextArea(); + textArea.setText(trade.getContractAsJson()); + String data = "Contract as json:\n"; + data += trade.getContractAsJson(); + data += "\n\nOther detail data:"; + if (!trade.isDepositPublished()) { + data += "\n\n" + (trade.getMaker() == trade.getBuyer() ? "Buyer" : "Seller") + " as maker reserve tx hex: " + trade.getMaker().getReserveTxHex(); + data += "\n\n" + (trade.getTaker() == trade.getBuyer() ? "Buyer" : "Seller") + " as taker reserve tx hex: " + trade.getTaker().getReserveTxHex(); + } + if (offer.isFiatOffer()) { + data += "\n\nBuyers witness hash,pub key ring hash: " + buyerWitnessHash + "," + buyerPubKeyRingHash; + data += "\nBuyers account age: " + buyersAccountAge; + data += "\nSellers witness hash,pub key ring hash: " + sellerWitnessHash + "," + sellerPubKeyRingHash; + data += "\nSellers account age: " + sellersAccountAge; + } - // TODO (woodser): include maker and taker deposit tx hex in contract? + // TODO (woodser): include maker and taker deposit tx hex in contract? // if (depositTx != null) { // String depositTxAsHex = Utils.HEX.encode(depositTx.bitcoinSerialize(true)); // data += "\n\nRaw deposit transaction as hex:\n" + depositTxAsHex; // } - data += "\n\nSelected arbitrator: " + DisputeAgentLookupMap.getMatrixUserName(contract.getArbitratorNodeAddress().getFullAddress()); - textArea.setText(data); - textArea.setPrefHeight(50); - textArea.setEditable(false); - textArea.setWrapText(true); - textArea.setPrefSize(800, 600); + data += "\n\nSelected arbitrator: " + trade.getArbitrator().getNodeAddress(); - Scene viewContractScene = new Scene(textArea); - Stage viewContractStage = new Stage(); - viewContractStage.setTitle(Res.get("shared.contract.title", trade.getShortId())); - viewContractStage.setScene(viewContractScene); - if (owner == null) - owner = MainView.getRootContainer(); - Scene rootScene = owner.getScene(); - viewContractStage.initOwner(rootScene.getWindow()); - viewContractStage.initModality(Modality.NONE); - viewContractStage.initStyle(StageStyle.UTILITY); - viewContractStage.setOpacity(0); - viewContractStage.show(); + textArea.setText(data); + textArea.setPrefHeight(50); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setPrefSize(800, 600); - Window window = rootScene.getWindow(); - double titleBarHeight = window.getHeight() - rootScene.getHeight(); - viewContractStage.setX(Math.round(window.getX() + (owner.getWidth() - viewContractStage.getWidth()) / 2) + 200); - viewContractStage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - viewContractStage.getHeight()) / 2) + 50); - // Delay display to next render frame to avoid that the popup is first quickly displayed in default position - // and after a short moment in the correct position - UserThread.execute(() -> viewContractStage.setOpacity(1)); + Scene viewContractScene = new Scene(textArea); + Stage viewContractStage = new Stage(); + viewContractStage.setTitle(Res.get("shared.contract.title", trade.getShortId())); + viewContractStage.setScene(viewContractScene); + if (owner == null) + owner = MainView.getRootContainer(); + Scene rootScene = owner.getScene(); + viewContractStage.initOwner(rootScene.getWindow()); + viewContractStage.initModality(Modality.NONE); + viewContractStage.initStyle(StageStyle.UTILITY); + viewContractStage.setOpacity(0); + viewContractStage.show(); - viewContractScene.setOnKeyPressed(ev -> { - if (ev.getCode() == KeyCode.ESCAPE) { - ev.consume(); - viewContractStage.hide(); - } - }); + Window window = rootScene.getWindow(); + double titleBarHeight = window.getHeight() - rootScene.getHeight(); + viewContractStage.setX(Math.round(window.getX() + (owner.getWidth() - viewContractStage.getWidth()) / 2) + 200); + viewContractStage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - viewContractStage.getHeight()) / 2) + 50); + // Delay display to next render frame to avoid that the popup is first quickly displayed in default position + // and after a short moment in the correct position + UserThread.execute(() -> viewContractStage.setOpacity(1)); + + viewContractScene.setOnKeyPressed(ev -> { + if (ev.getCode() == KeyCode.ESCAPE) { + ev.consume(); + viewContractStage.hide(); + } }); - } + }); closeButton.setOnAction(e -> { closeHandlerOptional.ifPresent(Runnable::run); diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 0fba85d1..21fdee33 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -445,11 +445,11 @@ public abstract class DisputeView extends ActivatableView { return FilterResult.SELLER_NODE_ADDRESS; } - if (dispute.getBuyerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { + if (dispute.getBuyerPaymentAccountPayload() != null && dispute.getBuyerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { return FilterResult.BUYER_ACCOUNT_DETAILS; } - if (dispute.getSellerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { + if (dispute.getSellerPaymentAccountPayload() != null && dispute.getSellerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { return FilterResult.SELLER_ACCOUNT_DETAILS; } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 725766ba..db6fd747 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1509,10 +1509,12 @@ message SignedOfferList { } message SignedOffer { - string offer_id = 1; - string reserve_tx_hash = 2; - string reserve_tx_hex = 3; - string arbitrator_signature = 4; + int64 time_stamp = 1; + string offer_id = 2; + string reserve_tx_hash = 3; + string reserve_tx_hex = 4; + repeated string reserve_tx_key_images = 5; + string arbitrator_signature = 6; } message OpenOffer {