Merge branch 'haveno-dex:master' into haveno-reto-dev

This commit is contained in:
boldsuck 2025-03-27 23:33:22 +01:00 committed by GitHub
commit 3bd0db513a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
90 changed files with 1957 additions and 591 deletions

View File

@ -1,7 +1,7 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Copyright (C) 2020 Haveno Dex
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@ -659,4 +659,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.

View File

@ -71,7 +71,7 @@ configure(subprojects) {
loggingVersion = '1.2'
lombokVersion = '1.18.30'
mockitoVersion = '5.10.0'
netlayerVersion = '700ec94f0f' // Tor browser version 14.0.3 and tor binary version: 0.4.8.13
netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14
protobufVersion = '3.19.1'
protocVersion = protobufVersion
pushyVersion = '0.13.2'
@ -610,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle'
version = '1.0.18-SNAPSHOT'
version = '1.0.19-SNAPSHOT'
jar.manifest.attributes(
"Implementation-Title": project.name,

View File

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version {
// The application versions
// We use semantic versioning with major, minor and patch
public static final String VERSION = "1.0.18";
public static final String VERSION = "1.0.19";
/**
* Holds a list of the tagged resource files for optimizing the getData requests.
@ -72,6 +72,25 @@ public class Version {
return false;
}
public static int compare(String version1, String version2) {
if (version1.equals(version2))
return 0;
else if (getMajorVersion(version1) > getMajorVersion(version2))
return 1;
else if (getMajorVersion(version1) < getMajorVersion(version2))
return -1;
else if (getMinorVersion(version1) > getMinorVersion(version2))
return 1;
else if (getMinorVersion(version1) < getMinorVersion(version2))
return -1;
else if (getPatchVersion(version1) > getPatchVersion(version2))
return 1;
else if (getPatchVersion(version1) < getPatchVersion(version2))
return -1;
else
return 0;
}
private static int getSubVersion(String version, int index) {
final String[] split = version.split("\\.");
checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version);
@ -91,8 +110,9 @@ public class Version {
// For the switch to version 2, offers created with the old version will become invalid and have to be canceled.
// For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening
// the Haveno app.
// VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
public static final int TRADE_PROTOCOL_VERSION = 1;
// Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1
// Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2
public static final int TRADE_PROTOCOL_VERSION = 2;
private static String p2pMessageVersion;
public static String getP2PMessageVersion() {

View File

@ -159,7 +159,7 @@ public class CoreOffersService {
}
OpenOffer getMyOffer(String id) {
return openOfferManager.getOpenOfferById(id)
return openOfferManager.getOpenOffer(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id)));
@ -265,6 +265,7 @@ public class CoreOffersService {
if (!seenKeyImages.add(keyImage)) {
for (Offer offer2 : offers) {
if (offer == offer2) continue;
if (offer2.getOfferPayload().getReserveTxKeyImages() == null) continue;
if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId());
duplicateFundedOffers.add(offer2);

View File

@ -178,6 +178,9 @@ public class DomainInitialisation {
closedTradableManager.onAllServicesInitialized();
failedTradesManager.onAllServicesInitialized();
filterManager.setFilterWarningHandler(filterWarningHandler);
filterManager.onAllServicesInitialized();
openOfferManager.onAllServicesInitialized();
balances.onAllServicesInitialized();
@ -199,10 +202,6 @@ public class DomainInitialisation {
priceFeedService.setCurrencyCodeOnInit();
priceFeedService.startRequestingPrices();
filterManager.setFilterWarningHandler(filterWarningHandler);
filterManager.onAllServicesInitialized();
mobileNotificationService.onAllServicesInitialized();
myOfferTakenEvents.onAllServicesInitialized();
tradeEvents.onAllServicesInitialized();

View File

@ -86,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler"));
havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg));
havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage));
havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler"));
havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler"));
havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList));
havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler"));
havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler"));

View File

@ -176,7 +176,7 @@ public class HavenoSetup {
private Consumer<PrivateNotificationPayload> displayPrivateNotificationHandler;
@Setter
@Nullable
private Runnable showPopupIfInvalidBtcConfigHandler;
private Runnable showPopupIfInvalidXmrConfigHandler;
@Setter
@Nullable
private Consumer<List<RevolutAccount>> revolutAccountsUpdateHandler;
@ -461,7 +461,7 @@ public class HavenoSetup {
havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet);
walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler,
showPopupIfInvalidBtcConfigHandler,
showPopupIfInvalidXmrConfigHandler,
() -> {},
() -> {});
}

View File

@ -117,7 +117,7 @@ public class WalletAppSetup {
void init(@Nullable Consumer<String> chainFileLockedExceptionHandler,
@Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler,
@Nullable Runnable showPopupIfInvalidBtcConfigHandler,
@Nullable Runnable showPopupIfInvalidXmrConfigHandler,
Runnable downloadCompleteHandler,
Runnable walletInitializedHandler) {
log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion());
@ -199,8 +199,8 @@ public class WalletAppSetup {
walletInitializedHandler.run();
},
exception -> {
if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) {
showPopupIfInvalidBtcConfigHandler.run();
if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) {
showPopupIfInvalidXmrConfigHandler.run();
} else {
walletServiceException.set(exception);
}

View File

@ -407,6 +407,10 @@ public class FilterManager {
.anyMatch(e -> e.equals(address));
}
public String getDisableTradeBelowVersion() {
return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion();
}
public boolean requireUpdateToNewVersionForTrading() {
if (getFilter() == null) {
return false;

View File

@ -97,6 +97,7 @@ import haveno.network.p2p.peers.PeerManager;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -236,7 +237,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void onAdded(Offer offer) {
// cancel offer if reserved funds spent
Optional<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
Optional<OpenOffer> openOfferOptional = getOpenOffer(offer.getId());
if (openOfferOptional.isPresent() && openOfferOptional.get().getState() != OpenOffer.State.RESERVED && offer.isReservedFundsSpent()) {
log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", offer.getId(), openOfferOptional.get().getState());
cancelOpenOffer(openOfferOptional.get(), null, null);
@ -573,7 +574,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// Remove from offerbook
public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Optional<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
Optional<OpenOffer> openOfferOptional = getOpenOffer(offer.getId());
if (openOfferOptional.isPresent()) {
cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler);
} else {
@ -686,7 +687,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
OpenOffer.State originalState,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
Optional<OpenOffer> openOfferOptional = getOpenOfferById(editedOffer.getId());
Optional<OpenOffer> openOfferOptional = getOpenOffer(editedOffer.getId());
if (openOfferOptional.isPresent()) {
OpenOffer openOffer = openOfferOptional.get();
@ -736,7 +737,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
doCancelOffer(openOffer, true);
}
// remove open offer which thaws its key images
// cancel open offer which thaws its key images
private void doCancelOffer(@NotNull OpenOffer openOffer, boolean resetAddressEntries) {
Offer offer = openOffer.getOffer();
offer.setState(Offer.State.REMOVED);
@ -750,7 +751,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// close open offer after key images spent
public void closeOpenOffer(Offer offer) {
getOpenOfferById(offer.getId()).ifPresent(openOffer -> {
getOpenOffer(offer.getId()).ifPresent(openOffer -> {
removeOpenOffer(openOffer);
openOffer.setState(OpenOffer.State.CLOSED);
xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId());
@ -813,14 +814,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return openOffers.getObservableList();
}
public Optional<OpenOffer> getOpenOfferById(String offerId) {
public Optional<OpenOffer> getOpenOffer(String offerId) {
synchronized (openOffers) {
return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst();
}
}
public boolean hasOpenOffer(String offerId) {
return getOpenOfferById(offerId).isPresent();
return getOpenOffer(offerId).isPresent();
}
public Optional<SignedOffer> getSignedOfferById(String offerId) {
@ -888,6 +889,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
List<String> errorMessages = new ArrayList<String>();
synchronized (processOffersLock) {
List<OpenOffer> openOffers = getOpenOffers();
removeOffersWithDuplicateKeyImages(openOffers);
for (OpenOffer pendingOffer : openOffers) {
if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts
@ -919,6 +921,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, THREAD_ID);
}
private void removeOffersWithDuplicateKeyImages(List<OpenOffer> openOffers) {
// collect offers with duplicate key images
Set<String> keyImages = new HashSet<>();
Set<OpenOffer> offersToRemove = new HashSet<>();
for (OpenOffer openOffer : openOffers) {
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue;
if (Collections.disjoint(keyImages, openOffer.getOffer().getOfferPayload().getReserveTxKeyImages())) {
keyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
} else {
offersToRemove.add(openOffer);
}
}
// remove offers with duplicate key images
for (OpenOffer offerToRemove : offersToRemove) {
log.warn("Removing open offer which has duplicate key images with other open offers: {}", offerToRemove.getId());
doCancelOffer(offerToRemove);
openOffers.remove(offerToRemove);
}
}
private void processPendingOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// skip if already processing
@ -987,26 +1011,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
setSplitOutputTx(openOffer, splitOutputTx);
}
// if not found, create tx to split exact output
if (splitOutputTx == null) {
if (openOffer.getSplitOutputTxHash() != null) {
log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash());
setSplitOutputTx(openOffer, null);
}
try {
splitOrSchedule(openOffers, openOffer, amountNeeded);
} catch (Exception e) {
log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage());
openOffer.getOffer().setState(Offer.State.INVALID);
errorMessageHandler.handleErrorMessage(e.getMessage());
return;
}
} else if (!splitOutputTx.isLocked()) {
// otherwise sign and post offer if split output available
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
// if wallet has exact available balance, try to sign and post directly
if (xmrWalletService.getAvailableBalance().equals(amountNeeded)) {
signAndPostOffer(openOffer, true, resultHandler, (errorMessage) -> {
splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler);
});
return;
} else {
splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler);
}
} else {
// sign and post offer if enough funds
@ -1017,11 +1031,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
} else if (openOffer.getScheduledTxHashes() == null) {
scheduleWithEarliestTxs(openOffers, openOffer);
resultHandler.handleResult(null);
return;
}
}
// handle result
resultHandler.handleResult(null);
} catch (Exception e) {
if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e);
errorMessageHandler.handleErrorMessage(e.getMessage());
@ -1087,13 +1100,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (output.isSpent() || output.isFrozen()) removeTxs.add(tx);
}
}
if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx);
if (!hasExactOutput(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx);
}
splitOutputTxs.removeAll(removeTxs);
return splitOutputTxs;
}
private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) {
private boolean hasExactOutput(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) {
boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery()
.setAccountIndex(0)
.setSubaddressIndex(preferredSubaddressIndex)
@ -1115,7 +1128,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return earliestUnscheduledTx;
}
private void splitOrSchedule(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) {
// if split tx not found and cannot reserve exact amount directly, create tx to split or reserve exact output
private void splitOrSchedule(MoneroTxWallet splitOutputTx, List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger amountNeeded, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
if (splitOutputTx == null) {
if (openOffer.getSplitOutputTxHash() != null) {
log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash());
setSplitOutputTx(openOffer, null);
}
try {
splitOrScheduleAux(openOffers, openOffer, amountNeeded);
resultHandler.handleResult(null);
return;
} catch (Exception e) {
log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage());
openOffer.getOffer().setState(Offer.State.INVALID);
errorMessageHandler.handleErrorMessage(e.getMessage());
return;
}
} else if (!splitOutputTx.isLocked()) {
// otherwise sign and post offer if split output available
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
return;
} else {
resultHandler.handleResult(null);
return;
}
}
private void splitOrScheduleAux(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) {
// handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getAvailableBalance().compareTo(offerReserveAmount) >= 0;
@ -1299,13 +1340,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setScheduledAmount(null);
requestPersistence();
resultHandler.handleResult(transaction);
if (!stopped) {
startPeriodicRepublishOffersTimer();
startPeriodicRefreshOffersTimer();
} else {
log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call.");
}
resultHandler.handleResult(transaction);
},
errorMessageHandler);
@ -1355,6 +1396,40 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
// verify max length of extra info
if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify the trade protocol version
if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) {
errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify the min version number
if (filterManager.getDisableTradeBelowVersion() != null) {
if (Version.compare(request.getOfferPayload().getVersionNr(), filterManager.getDisableTradeBelowVersion()) < 0) {
errorMessage = "Offer version number is too low: " + request.getOfferPayload().getVersionNr() + " < " + filterManager.getDisableTradeBelowVersion();
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
}
// verify the max version number
if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) {
errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION;
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker and taker fees
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
if (hasBuyerAsTakerWithoutDeposit) {
@ -1557,6 +1632,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
// Don't allow trade start if not connected to Monero node
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) {
errorMessage = "We got a handleOfferAvailabilityRequest but we are not connected to a Monero node.";
log.info(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
if (stopped) {
errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call.";
log.debug(errorMessage);
@ -1575,7 +1658,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
try {
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
Optional<OpenOffer> openOfferOptional = getOpenOffer(request.offerId);
AvailabilityResult availabilityResult;
byte[] makerSignature = null;
if (openOfferOptional.isPresent()) {
@ -1801,27 +1884,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.getChallengeHash(),
updatedExtraDataMap,
protocolVersion,
originalOfferPayload.getArbitratorSigner(),
originalOfferPayload.getArbitratorSignature(),
originalOfferPayload.getReserveTxKeyImages(),
null,
null,
null,
originalOfferPayload.getExtraInfo());
// Save states from original data to use for the updated
Offer.State originalOfferState = originalOffer.getState();
OpenOffer.State originalOpenOfferState = originalOpenOffer.getState();
// cancel old offer
log.info("Canceling outdated offer id={}", originalOffer.getId());
doCancelOffer(originalOpenOffer, false);
// remove old offer
originalOffer.setState(Offer.State.REMOVED);
originalOpenOffer.setState(OpenOffer.State.CANCELED);
removeOpenOffer(originalOpenOffer);
// Create new Offer
// create new offer
Offer updatedOffer = new Offer(updatedPayload);
updatedOffer.setPriceFeedService(priceFeedService);
updatedOffer.setState(originalOfferState);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice());
updatedOpenOffer.setState(originalOpenOfferState);
addOpenOffer(updatedOpenOffer);
requestPersistence();
@ -1961,6 +2037,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
private boolean preventedFromPublishing(OpenOffer openOffer) {
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true;
return openOffer.isDeactivated() || openOffer.isCanceled() || openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null;
}
@ -1983,25 +2060,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (periodicRefreshOffersTimer == null)
periodicRefreshOffersTimer = UserThread.runPeriodically(() -> {
if (!stopped) {
int size = openOffers.size();
//we clone our list as openOffers might change during our delayed call
final ArrayList<OpenOffer> openOffersList = new ArrayList<>(openOffers.getList());
for (int i = 0; i < size; i++) {
// we delay to avoid reaching throttle limits
// roughly 4 offers per second
long delay = 300;
final long minDelay = (i + 1) * delay;
final long maxDelay = (i + 2) * delay;
final OpenOffer openOffer = openOffersList.get(i);
UserThread.runAfterRandomDelay(() -> {
// we need to check if in the meantime the offer has been removed
boolean contained = false;
synchronized (openOffers) {
contained = openOffers.contains(openOffer);
}
if (contained) maybeRefreshOffer(openOffer, 0, 1);
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
synchronized (openOffers) {
int size = openOffers.size();
//we clone our list as openOffers might change during our delayed call
final ArrayList<OpenOffer> openOffersList = new ArrayList<>(openOffers.getList());
for (int i = 0; i < size; i++) {
// we delay to avoid reaching throttle limits
// roughly 4 offers per second
long delay = 300;
final long minDelay = (i + 1) * delay;
final long maxDelay = (i + 2) * delay;
final OpenOffer openOffer = openOffersList.get(i);
UserThread.runAfterRandomDelay(() -> {
// we need to check if in the meantime the offer has been removed
boolean contained = false;
synchronized (openOffers) {
contained = openOffers.contains(openOffer);
}
if (contained) maybeRefreshOffer(openOffer, 0, 1);
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
}
}
} else {
log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call.");

View File

@ -87,6 +87,9 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
try {
//if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (IllegalStateException e) {
log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage());
throw e;
} catch (Exception e) {
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
model.getXmrWalletService().handleWalletError(e, sourceConnection);

View File

@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
offer.getOfferPayload().getReserveTxKeyImages(),
returnAddress);
// send request to least used arbitrators until success
// send request to random arbitrators until success
sendSignOfferRequests(request, () -> {
complete();
}, (errorMessage) -> {

View File

@ -52,7 +52,8 @@ public class ProvidersRepository {
private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/";
private static final List<String> DEFAULT_NODES = Arrays.asList(
"http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/" // Cake
"http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake
"http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck
);
private final Config config;

View File

@ -399,13 +399,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage);
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// create dispute opened message
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
@ -578,8 +571,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
}
// update multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
// update opener's multisig hex
TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender;
if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex());
// add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
@ -605,7 +599,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (trade.isArbitrator()) {
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex());
}
tradeManager.requestPersistence();
errorMessage = null;

View File

@ -62,7 +62,6 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Contract;
import haveno.core.trade.HavenoUtils;
@ -464,14 +463,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// check daemon connection
trade.verifyDaemonConnection();
// adapt from 1.0.6 to 1.0.7 which changes field usage
// TODO: remove after future updates to allow old trades to clear
if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex());
trade.setPayoutTxHex(null);
}
// sign arbitrator-signed payout tx
if (trade.getPayoutTxHex() == null) {
try {

View File

@ -196,7 +196,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
tradeManager.requestPersistence();
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);

View File

@ -34,7 +34,7 @@ import java.util.Optional;
public final class DisputeOpenedMessage extends DisputeMessage {
private final Dispute dispute;
private final NodeAddress senderNodeAddress;
private final String updatedMultisigHex;
private final String openerUpdatedMultisigHex;
private final PaymentSentMessage paymentSentMessage;
public DisputeOpenedMessage(Dispute dispute,
@ -67,7 +67,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
super(messageVersion, uid, supportType);
this.dispute = dispute;
this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex;
this.openerUpdatedMultisigHex = updatedMultisigHex;
this.paymentSentMessage = paymentSentMessage;
}
@ -78,7 +78,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
.setDispute(dispute.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType))
.setUpdatedMultisigHex(updatedMultisigHex);
.setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex);
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build();
}
@ -91,7 +91,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()),
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
}
@ -108,7 +108,7 @@ public final class DisputeOpenedMessage extends DisputeMessage {
",\n DisputeOpenedMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
",\n updatedMultisigHex=" + updatedMultisigHex +
",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex +
",\n paymentSentMessage=" + paymentSentMessage +
"\n} " + super.toString();
}

View File

@ -196,7 +196,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
tradeManager.requestPersistence();
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
@ -205,7 +205,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}

View File

@ -143,6 +143,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published
private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 10;
protected final Object pollLock = new Object();
protected static final Object importMultisigLock = new Object();
private boolean pollInProgress;
@ -194,7 +195,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED);
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
BUYER_RECEIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED);
@NotNull
public Phase getPhase() {
@ -602,12 +604,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
// notified from TradeProtocol of ack messages
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
// notified from TradeProtocol of ack messages
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
for (TradeListener listener : new ArrayList<TradeListener>(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception
listener.onAckMessage(ackMessage, sender);
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -617,8 +619,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
public void initialize(ProcessModelServiceProvider serviceProvider) {
if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized");
// done if payout unlocked and marked complete
if (isPayoutUnlocked() && isCompleted()) {
// skip initialization if trade is complete
// starting in v1.0.19, seller resends payment received message until acked or stored in mailbox
if (isFinished()) {
clearAndShutDown();
return;
}
@ -633,16 +636,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
ThreadUtils.execute(() -> onConnectionChanged(connection), getId());
});
// reset buyer's payment sent state if no ack receive
if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
}
// reset states if no ack receive
if (!isPayoutPublished()) {
// reset seller's payment received state if no ack receive
if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
// reset buyer's payment sent state if no ack receive
if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
}
// reset seller's payment received state if no ack receive
if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
resetToPaymentSentState();
}
}
// handle trade state events
@ -732,15 +739,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
xmrWalletService.addWalletListener(idlePayoutSyncer);
}
// TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed in v1.0.19 so this check can be removed?
// TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check can be removed?
if (isBuyer()) {
MessageState expectedState = getPaymentSentMessageState();
if (expectedState != null && expectedState != processModel.getPaymentSentMessageStateProperty().get()) {
log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStateProperty().get());
processModel.getPaymentSentMessageStateProperty().set(expectedState);
if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) {
log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get());
getSeller().getPaymentSentMessageStateProperty().set(expectedState);
}
}
// handle confirmations
walletHeight.addListener((observable, oldValue, newValue) -> {
importMultisigHexIfScheduled();
});
// trade is initialized
isInitialized = true;
@ -765,11 +777,30 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
// start polling if deposit requested
if (isDepositRequested()) tryInitPolling();
// init syncing if deposit requested
if (isDepositRequested()) {
tryInitSyncing();
}
isFullyInitialized = true;
}
public boolean isFinished() {
return isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages();
}
public void resetToPaymentSentState() {
setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
for (TradePeer peer : getAllPeers()) peer.setPaymentReceivedMessage(null);
setPayoutTxHex(null);
}
public void reprocessApplicableMessages() {
if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return;
getProtocol().maybeReprocessPaymentSentMessage(false);
getProtocol().maybeReprocessPaymentReceivedMessage(false);
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
}
public void awaitInitialized() {
while (!isFullyInitialized) HavenoUtils.waitFor(100); // TODO: use proper notification and refactor isInitialized, fullyInitialized, and arbitrator idling
}
@ -1077,6 +1108,26 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
public void scheduleImportMultisigHex() {
processModel.setImportMultisigHexScheduled(true);
requestPersistence();
}
private void importMultisigHexIfScheduled() {
if (!isInitialized || isShutDownStarted) return;
if (!isDepositsConfirmed() || getMaker().getDepositTx() == null) return;
if (walletHeight.get() - getMaker().getDepositTx().getHeight() < NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT) return;
ThreadUtils.execute(() -> {
if (!isInitialized || isShutDownStarted) return;
synchronized (getLock()) {
if (processModel.isImportMultisigHexScheduled()) {
processModel.setImportMultisigHexScheduled(false);
ThreadUtils.submitToPool(() -> importMultisigHex());
}
}
}, getId());
}
public void importMultisigHex() {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
@ -1089,10 +1140,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
@ -1141,6 +1192,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0]));
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage);
}
// remove scheduled import
processModel.setImportMultisigHexScheduled(false);
} catch (MoneroError e) {
// import multisig hex individually if one is invalid
@ -1335,7 +1389,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if not signed
if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if id currently unknown
// verify payout tx has exactly 2 destinations
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations");
@ -1366,6 +1420,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit);
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// update payout tx
updatePayout(payoutTx);
// check connection
boolean doSign = sign && getPayoutTxHex() == null;
if (doSign || publish) verifyDaemonConnection();
@ -1374,28 +1431,36 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (doSign) {
// sign tx
String signedPayoutTxHex;
try {
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new IllegalArgumentException("Error signing payout tx, signed multisig hex is null");
setPayoutTxHex(result.getSignedMultisigTxHex());
signedPayoutTxHex = result.getSignedMultisigTxHex();
} catch (Exception e) {
throw new IllegalStateException(e);
}
// verify miner fee is within tolerance unless outdated offer version
if (getOffer().getOfferPayload().getProtocolVersion() >= 2) {
// verify fee is within tolerance by recreating payout tx
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId());
saveWallet(); // save wallet before creating fee estimate tx
MoneroTxWallet feeEstimateTx = createPayoutTx();
BigInteger feeEstimate = feeEstimateTx.getFee();
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
}
// set signed payout tx hex
setPayoutTxHex(signedPayoutTxHex);
// describe result
describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex());
payoutTx = describedTxSet.getTxs().get(0);
updatePayout(payoutTx);
// verify fee is within tolerance by recreating payout tx
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId());
saveWallet(); // save wallet before creating fee estimate tx
MoneroTxWallet feeEstimateTx = createPayoutTx();
BigInteger feeEstimate = feeEstimateTx.getFee();
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
}
// save trade state
@ -1506,7 +1571,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
peer.setUpdatedMultisigHex(null);
peer.setDisputeClosedMessage(null);
peer.setPaymentSentMessage(null);
peer.setPaymentReceivedMessage(null);
if (peer.isPaymentReceivedMessageReceived()) peer.setPaymentReceivedMessage(null);
}
}
@ -1604,7 +1669,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
// unreserve maker's open offer
Optional<OpenOffer> openOffer = processModel.getOpenOfferManager().getOpenOfferById(this.getId());
Optional<OpenOffer> openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId());
if (this instanceof MakerTrade && openOffer.isPresent()) {
processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get());
}
@ -1618,15 +1683,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// done if wallet already deleted
if (!walletExists()) return;
// move to failed trades
processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this);
// set error height
if (processModel.getTradeProtocolErrorHeight() == 0) {
log.warn("Scheduling to remove trade if unfunded for {} {} from height {}", getClass().getSimpleName(), getId(), xmrConnectionService.getLastInfo().getHeight());
processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight());
processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); // height denotes scheduled error handling
}
// move to failed trades
processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this);
requestPersistence();
// listen for deposits published to restore trade
protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> {
if (isDepositsPublished()) {
@ -1680,10 +1746,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
});
}
public boolean isProtocolErrorHandlingScheduled() {
return processModel.getTradeProtocolErrorHeight() > 0;
}
private void restoreDepositsPublishedTrade() {
// close open offer
if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) {
if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOffer(getId()).isPresent()) {
log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId());
processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(getOffer()));
}
@ -1868,10 +1938,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
getSeller().setPayoutTxFee(splitTxFee);
getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount()));
getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee()));
} else if (getDisputeState().isClosed()) {
} else {
DisputeResult disputeResult = getDisputeResult();
if (disputeResult == null) log.warn("Dispute result is not set for {} {}", getClass().getSimpleName(), getId());
else {
if (disputeResult != null) {
BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee());
getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]);
getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]);
@ -2015,9 +2084,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator");
}
public MessageState getPaymentSentMessageState() {
private MessageState getPaymentSentMessageState() {
if (isPaymentReceived()) return MessageState.ACKNOWLEDGED;
if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED;
if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED;
switch (state) {
case BUYER_SENT_PAYMENT_SENT_MSG:
return MessageState.SENT;
@ -2156,7 +2225,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
public boolean isPaymentSent() {
return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal();
return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal() && getState() != State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG;
}
public boolean hasPaymentReceivedMessage() {
@ -2174,7 +2243,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
public boolean isPaymentReceived() {
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal();
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal() && getState() != State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG;
}
public boolean isPayoutPublished() {
@ -2345,7 +2414,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
return tradeAmountTransferred();
}
public boolean tradeAmountTransferred() {
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private boolean tradeAmountTransferred() {
return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER);
}
@ -2361,11 +2435,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
// lazy initialization
private ObjectProperty<BigInteger> getAmountProperty() {
if (tradeAmountProperty == null)
@ -2410,11 +2479,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// sync and reprocess messages on new thread
if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) {
ThreadUtils.execute(() -> tryInitPolling(), getId());
ThreadUtils.execute(() -> tryInitSyncing(), getId());
}
}
}
private void tryInitPolling() {
private void tryInitSyncing() {
if (isShutDownStarted) return;
// set known deposit txs
@ -2423,23 +2493,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// start polling
if (!isIdling()) {
tryInitPollingAux();
doTryInitSyncing();
} else {
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling
UserThread.runAfter(() -> ThreadUtils.execute(() -> {
if (!isShutDownStarted) tryInitPollingAux();
if (!isShutDownStarted) doTryInitSyncing();
}, getId()), startSyncingInMs / 1000l);
}
}
private void tryInitPollingAux() {
private void doTryInitSyncing() {
if (!wasWalletSynced) trySyncWallet(true);
updatePollPeriod();
// reprocess pending payout messages
this.getProtocol().maybeReprocessPaymentReceivedMessage(false);
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
startPolling();
}
@ -2790,7 +2855,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (!isShutDownStarted) wallet = getWallet();
restartInProgress = false;
pollWallet();
if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId());
if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitSyncing(), getId());
}
private void setStateDepositsSeen() {

View File

@ -450,8 +450,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
// skip if marked as failed
if (failedTradesManager.getObservableList().contains(trade)) {
// skip if failed and error handling not scheduled
if (failedTradesManager.getObservableList().contains(trade) && !trade.isProtocolErrorHandlingScheduled()) {
log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId());
tradesToSkip.add(trade);
return;
@ -460,8 +460,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// initialize trade
initPersistedTrade(trade);
// remove trade if protocol didn't initialize
if (getOpenTradeByUid(trade.getUid()).isPresent() && !trade.isDepositsPublished()) {
// record if protocol didn't initialize
if (!trade.isDepositsPublished()) {
uninitializedTrades.add(trade);
}
} catch (Exception e) {
@ -556,7 +556,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) {
// get open offer
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId());
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId());
if (!openOfferOptional.isPresent()) return;
OpenOffer openOffer = openOfferOptional.get();
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
@ -747,7 +747,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
try {
Validator.nonEmptyStringOf(request.getOfferId());
@ -766,7 +766,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) {
log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
try {
Validator.nonEmptyStringOf(request.getOfferId());
@ -923,8 +923,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
requestPersistence();
}, errorMessage -> {
log.warn("Taker error during trade initialization: " + errorMessage);
xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error
trade.onProtocolError();
xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling
errorMessageHandler.handleErrorMessage(errorMessage);
});
@ -1285,6 +1285,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
}
public boolean hasFailedScheduledTrade(String offerId) {
synchronized (failedTradesManager) {
return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled();
}
}
public Optional<Trade> getOpenTradeByUid(String tradeUid) {
synchronized (tradableList) {
return tradableList.stream().filter(e -> e.getUid().equals(tradeUid)).findFirst();

View File

@ -43,7 +43,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
System.out.println("ArbitratorProtocol.handleInitTradeRequest()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();
@ -78,7 +78,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
}
public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId());
log.info(TradeProtocol.LOG_HIGHLIGHT + "handleDepositRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();

View File

@ -60,8 +60,8 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer,
ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
ThreadUtils.execute(() -> {
log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer);
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();
this.errorMessageHandler = errorMessageHandler;

View File

@ -68,7 +68,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
@Override
public void onTakeOffer(TradeResultHandler tradeResultHandler,
ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();
@ -99,7 +99,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
@Override
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer);
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();

View File

@ -119,7 +119,7 @@ public class BuyerProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
System.out.println("BuyerProtocol.onPaymentSent()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();

View File

@ -44,6 +44,7 @@ import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.filter.FilterManager;
import haveno.core.network.MessageState;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOfferManager;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.PaymentAccountPayload;
@ -73,6 +74,9 @@ import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not
@ -90,6 +94,7 @@ public class ProcessModel implements Model, PersistablePayload {
transient private ProcessModelServiceProvider provider;
transient private TradeManager tradeManager;
transient private Offer offer;
transient public Throwable error;
// Added in v1.4.0
// MessageState of the last message sent from the seller to the buyer in the take offer process.
@ -158,15 +163,14 @@ public class ProcessModel implements Model, PersistablePayload {
@Getter
@Setter
private long tradeProtocolErrorHeight;
// We want to indicate the user the state of the message delivery of the
// PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet.
// To enable that even after restart we persist the state.
@Getter
@Setter
private ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@Setter
private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private boolean importMultisigHexScheduled;
private ObjectProperty<Boolean> paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false);
@Deprecated
private ObjectProperty<MessageState> paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@Deprecated
private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED);
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer());
@ -188,6 +192,31 @@ public class ProcessModel implements Model, PersistablePayload {
this.offer = offer;
this.provider = provider;
this.tradeManager = tradeManager;
for (TradePeer peer : getTradePeers()) {
peer.applyTransient(tradeManager);
}
// migrate deprecated fields to new model for v1.0.19
if (paymentSentMessageStatePropertySeller.get() != MessageState.UNDEFINED && getSeller().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) {
getSeller().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertySeller.get());
tradeManager.requestPersistence();
}
if (paymentSentMessageStatePropertyArbitrator.get() != MessageState.UNDEFINED && getArbitrator().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) {
getArbitrator().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertyArbitrator.get());
tradeManager.requestPersistence();
}
}
private List<TradePeer> getTradePeers() {
return Arrays.asList(maker, taker, arbitrator);
}
private TradePeer getBuyer() {
return offer.getDirection() == OfferDirection.BUY ? maker : taker;
}
private TradePeer getSeller() {
return offer.getDirection() == OfferDirection.BUY ? taker : maker;
}
@ -203,11 +232,12 @@ public class ProcessModel implements Model, PersistablePayload {
.setPubKeyRing(pubKeyRing.toProtoMessage())
.setUseSavingsWallet(useSavingsWallet)
.setFundsNeededForTrade(fundsNeededForTrade)
.setPaymentSentMessageState(paymentSentMessageStateProperty.get().name())
.setPaymentSentMessageStateSeller(paymentSentMessageStatePropertySeller.get().name())
.setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name())
.setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation)
.setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation)
.setTradeProtocolErrorHeight(tradeProtocolErrorHeight);
.setTradeProtocolErrorHeight(tradeProtocolErrorHeight)
.setImportMultisigHexScheduled(importMultisigHexScheduled);
Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage()));
Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage()));
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage()));
@ -231,6 +261,7 @@ public class ProcessModel implements Model, PersistablePayload {
processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation());
processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation());
processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight());
processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled());
// nullable
processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature()));
@ -240,14 +271,13 @@ public class ProcessModel implements Model, PersistablePayload {
processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress()));
processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress()));
String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState());
MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString);
processModel.setPaymentSentMessageState(paymentSentMessageState);
// deprecated fields need to be read in order to migrate to new fields
String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller());
MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString);
processModel.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateSeller);
String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator());
MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString);
processModel.setPaymentSentMessageStateArbitrator(paymentSentMessageStateArbitrator);
processModel.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateArbitrator);
return processModel;
}
@ -274,32 +304,8 @@ public class ProcessModel implements Model, PersistablePayload {
return getP2PService().getAddress();
}
void setPaymentSentAckMessage(AckMessage ackMessage) {
MessageState messageState = ackMessage.isSuccess() ?
MessageState.ACKNOWLEDGED :
MessageState.FAILED;
setPaymentSentMessageState(messageState);
}
void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) {
MessageState messageState = ackMessage.isSuccess() ?
MessageState.ACKNOWLEDGED :
MessageState.FAILED;
setPaymentSentMessageStateArbitrator(messageState);
}
public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) {
this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
}
public void setPaymentSentMessageStateArbitrator(MessageState paymentSentMessageStateProperty) {
this.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateProperty);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
public boolean isPaymentReceivedMessagesReceived() {
return getArbitrator().isPaymentReceivedMessageReceived() && getBuyer().isPaymentReceivedMessageReceived();
}
void setDepositTxSentAckMessage(AckMessage ackMessage) {

View File

@ -65,7 +65,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer,
ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer);
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();

View File

@ -68,7 +68,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
@Override
public void onTakeOffer(TradeResultHandler tradeResultHandler,
ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();
@ -99,7 +99,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
@Override
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer);
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();

View File

@ -53,6 +53,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SellerProtocol extends DisputeProtocol {
enum SellerEvent implements FluentProtocol.Event {
STARTUP,
DEPOSIT_TXS_CONFIRMED,
@ -69,31 +70,37 @@ public class SellerProtocol extends DisputeProtocol {
// re-send payment received message if payout not published
ThreadUtils.execute(() -> {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
if (!needsToResendPaymentReceivedMessages()) return;
synchronized (trade.getLock()) {
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) {
latchTrade();
given(anyPhase(Trade.Phase.PAYMENT_RECEIVED)
.with(SellerEvent.STARTUP))
.setup(tasks(
SellerSendPaymentReceivedMessageToBuyer.class,
SellerSendPaymentReceivedMessageToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
unlatchTrade();
},
(errorMessage) -> {
log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage);
unlatchTrade();
})))
.executeTasks();
awaitTradeLatch();
}
if (!needsToResendPaymentReceivedMessages()) return;
latchTrade();
given(anyPhase(Trade.Phase.PAYMENT_RECEIVED)
.with(SellerEvent.STARTUP))
.setup(tasks(
SellerSendPaymentReceivedMessageToBuyer.class,
SellerSendPaymentReceivedMessageToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
unlatchTrade();
},
(errorMessage) -> {
log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage);
unlatchTrade();
})))
.executeTasks();
awaitTradeLatch();
}
}, trade.getId());
}
public boolean needsToResendPaymentReceivedMessages() {
return !trade.isShutDownStarted() && trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.getProcessModel().isPaymentReceivedMessagesReceived() && resendPaymentReceivedMessagesEnabled();
}
private boolean resendPaymentReceivedMessagesEnabled() {
return trade.getOffer().getOfferPayload().getProtocolVersion() >= 2;
}
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
@ -115,7 +122,7 @@ public class SellerProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
log.info("SellerProtocol.onPaymentReceived()");
log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
latchTrade();
@ -137,7 +144,7 @@ public class SellerProtocol extends DisputeProtocol {
resultHandler.handleResult();
}, (errorMessage) -> {
log.warn("Error confirming payment received, reverting state to {}, error={}", Trade.State.BUYER_SENT_PAYMENT_SENT_MSG, errorMessage);
trade.setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
trade.resetToPaymentSentState();
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT))

View File

@ -24,12 +24,17 @@ import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil;
import haveno.common.proto.persistable.PersistablePayload;
import haveno.core.account.witness.AccountAgeWitness;
import haveno.core.network.MessageState;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.trade.TradeManager;
import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage;
import haveno.network.p2p.AckMessage;
import haveno.network.p2p.NodeAddress;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -57,6 +62,7 @@ public final class TradePeer implements PersistablePayload {
@Nullable
transient private byte[] preparedDepositTx;
transient private MoneroTxWallet depositTx;
transient private TradeManager tradeManager;
// Persistable mutable
@Nullable
@ -96,7 +102,6 @@ public final class TradePeer implements PersistablePayload {
@Getter
private DisputeClosedMessage disputeClosedMessage;
// added in v 0.6
@Nullable
private byte[] accountAgeWitnessNonce;
@ -142,13 +147,32 @@ public final class TradePeer implements PersistablePayload {
private long payoutAmount;
@Nullable
private String updatedMultisigHex;
@Getter
@Deprecated
private boolean depositsConfirmedMessageAcked;
// We want to indicate the user the state of the message delivery of the payment
// confirmation messages. We do an automatic re-send in case it was not ACKed yet.
// To enable that even after restart we persist the state.
@Setter
boolean depositsConfirmedMessageAcked;
private ObjectProperty<MessageState> depositsConfirmedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@Setter
private ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@Setter
private ObjectProperty<MessageState> paymentReceivedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
public TradePeer() {
}
public void applyTransient(TradeManager tradeManager) {
this.tradeManager = tradeManager;
// migrate deprecated fields to new model for v1.0.19
if (depositsConfirmedMessageAcked && depositsConfirmedMessageStateProperty.get() == MessageState.UNDEFINED) {
depositsConfirmedMessageStateProperty.set(MessageState.ACKNOWLEDGED);
tradeManager.requestPersistence();
}
}
public BigInteger getDepositTxFee() {
return BigInteger.valueOf(depositTxFee);
}
@ -181,6 +205,60 @@ public final class TradePeer implements PersistablePayload {
this.payoutAmount = payoutAmount.longValueExact();
}
void setDepositsConfirmedAckMessage(AckMessage ackMessage) {
MessageState messageState = ackMessage.isSuccess() ?
MessageState.ACKNOWLEDGED :
MessageState.FAILED;
setDepositsConfirmedMessageState(messageState);
}
void setPaymentSentAckMessage(AckMessage ackMessage) {
MessageState messageState = ackMessage.isSuccess() ?
MessageState.ACKNOWLEDGED :
MessageState.FAILED;
setPaymentSentMessageState(messageState);
}
void setPaymentReceivedAckMessage(AckMessage ackMessage) {
MessageState messageState = ackMessage.isSuccess() ?
MessageState.ACKNOWLEDGED :
MessageState.FAILED;
setPaymentReceivedMessageState(messageState);
}
public void setDepositsConfirmedMessageState(MessageState depositsConfirmedMessageStateProperty) {
this.depositsConfirmedMessageStateProperty.set(depositsConfirmedMessageStateProperty);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
}
public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) {
this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
}
public void setPaymentReceivedMessageState(MessageState paymentReceivedMessageStateProperty) {
this.paymentReceivedMessageStateProperty.set(paymentReceivedMessageStateProperty);
if (tradeManager != null) {
tradeManager.requestPersistence();
}
}
public boolean isDepositsConfirmedMessageAcked() {
return depositsConfirmedMessageStateProperty.get() == MessageState.ACKNOWLEDGED;
}
public boolean isPaymentSentMessageAcked() {
return paymentSentMessageStateProperty.get() == MessageState.ACKNOWLEDGED;
}
public boolean isPaymentReceivedMessageReceived() {
return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX;
}
@Override
public Message toProtoMessage() {
final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder();
@ -221,6 +299,9 @@ public final class TradePeer implements PersistablePayload {
Optional.ofNullable(payoutTxFee).ifPresent(e -> builder.setPayoutTxFee(payoutTxFee));
Optional.ofNullable(payoutAmount).ifPresent(e -> builder.setPayoutAmount(payoutAmount));
builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked);
builder.setDepositsConfirmedMessageState(depositsConfirmedMessageStateProperty.get().name());
builder.setPaymentSentMessageState(paymentSentMessageStateProperty.get().name());
builder.setPaymentReceivedMessageState(paymentReceivedMessageStateProperty.get().name());
builder.setCurrentDate(currentDate);
return builder.build();
@ -270,6 +351,19 @@ public final class TradePeer implements PersistablePayload {
tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()));
tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee()));
tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount()));
String depositsConfirmedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDepositsConfirmedMessageState());
MessageState depositsConfirmedMessageState = ProtoUtil.enumFromProto(MessageState.class, depositsConfirmedMessageStateString);
tradePeer.setDepositsConfirmedMessageState(depositsConfirmedMessageState);
String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState());
MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString);
tradePeer.setPaymentSentMessageState(paymentSentMessageState);
String paymentReceivedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentReceivedMessageState());
MessageState paymentReceivedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentReceivedMessageStateString);
tradePeer.setPaymentReceivedMessageState(paymentReceivedMessageState);
return tradePeer;
}
}

View File

@ -96,6 +96,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
private static final String TIMEOUT_REACHED = "Timeout reached.";
public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions
public static final long REPROCESS_DELAY_MS = 5000;
public static final String LOG_HIGHLIGHT = "\u001B[0m"; // terminal default
protected final ProcessModel processModel;
protected final Trade trade;
@ -106,6 +107,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected ErrorMessageHandler errorMessageHandler;
private boolean depositsConfirmedTasksCalled;
private int reprocessPaymentSentMessageCount;
private int reprocessPaymentReceivedMessageCount;
///////////////////////////////////////////////////////////////////////////////////////////
@ -124,12 +126,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) {
log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid());
ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId());
handle(message, peerNodeAddress);
}
protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid());
ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId());
handle(message, peerNodeAddress);
}
private void handle(TradeMessage message, NodeAddress peerNodeAddress) {
@ -163,7 +165,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
} else if (networkEnvelope instanceof AckMessage) {
onAckMessage((AckMessage) networkEnvelope, peer);
trade.onAckMessage((AckMessage) networkEnvelope, peer); // notify trade listeners
}
}
@ -208,11 +209,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress());
} else if (mailboxMessage instanceof AckMessage) {
AckMessage ackMessage = (AckMessage) mailboxMessage;
if (!trade.isCompleted()) {
// We only apply the msg if we have not already completed the trade
onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress());
}
// In any case we remove the msg
onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress());
processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(ackMessage);
log.info("Remove {} from the P2P network.", ackMessage.getClass().getSimpleName());
}
@ -240,7 +237,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected void onInitialized() {
// listen for direct messages unless completed
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
if (!trade.isFinished()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
// initialize trade
synchronized (trade.getLock()) {
@ -250,6 +247,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this);
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
// reprocess applicable messages
trade.reprocessApplicableMessages();
}
// send deposits confirmed message if applicable
@ -279,24 +279,46 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}, trade.getId());
}
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
public boolean needsToResendPaymentReceivedMessages() {
return false; // seller protocol overrides
}
public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) {
if (trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> {
if (trade.isShutDownStarted()) return;
synchronized (trade.getLock()) {
// skip if no need to reprocess
if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) {
if (trade.isShutDownStarted() || trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) {
return;
}
log.warn("Reprocessing payment received message for {} {}", trade.getClass().getSimpleName(), trade.getId());
log.warn("Reprocessing PaymentSentMessage for {} {}", trade.getClass().getSimpleName(), trade.getId());
handle(trade.getBuyer().getPaymentSentMessage(), trade.getBuyer().getPaymentSentMessage().getSenderNodeAddress(), reprocessOnError);
}
}, trade.getId());
}
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
if (trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> {
if (trade.isShutDownStarted()) return;
synchronized (trade.getLock()) {
// skip if no need to reprocess
if (trade.isShutDownStarted() || trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) {
return;
}
log.warn("Reprocessing PaymentReceivedMessage for {} {}", trade.getClass().getSimpleName(), trade.getId());
handle(trade.getSeller().getPaymentReceivedMessage(), trade.getSeller().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError);
}
}, trade.getId());
}
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.info(LOG_HIGHLIGHT + "handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender);
trade.addInitProgressStep();
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
@ -333,7 +355,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.info(LOG_HIGHLIGHT + "handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender);
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
@ -376,7 +398,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.info(LOG_HIGHLIGHT + "handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender);
trade.addInitProgressStep();
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
@ -422,7 +444,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.info(LOG_HIGHLIGHT + "handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender);
trade.addInitProgressStep();
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
@ -452,7 +474,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
public void handle(DepositsConfirmedMessage message, NodeAddress sender) {
System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.info(LOG_HIGHLIGHT + "handle(DepositsConfirmedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender);
if (!trade.isInitialized() || trade.isShutDown()) return;
ThreadUtils.execute(() -> {
synchronized (trade.getLock()) {
@ -481,8 +503,26 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// received by seller and arbitrator
protected void handle(PaymentSentMessage message, NodeAddress peer) {
System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
if (!trade.isInitialized() || trade.isShutDown()) return;
handle(message, peer, true);
}
// received by seller and arbitrator
protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) {
log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer);
// validate signature
try {
HavenoUtils.verifyPaymentSentMessage(trade, message);
} catch (Throwable t) {
log.warn("Ignoring PaymentSentMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage());
return;
}
// save message for reprocessing
trade.getBuyer().setPaymentSentMessage(message);
trade.requestPersistence();
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) {
log.warn("Ignoring PaymentSentMessage since not seller or arbitrator");
return;
@ -494,7 +534,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return;
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
handleTaskRunnerSuccess(peer, message);
@ -521,7 +561,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
handleTaskRunnerSuccess(peer, message);
},
(errorMessage) -> {
handleTaskRunnerFault(peer, message, errorMessage);
log.warn("Error processing payment sent message: " + errorMessage);
processModel.getTradeManager().requestPersistence();
// schedule to reprocess message unless deleted
if (trade.getBuyer().getPaymentSentMessage() != null) {
UserThread.runAfter(() -> {
reprocessPaymentSentMessageCount++;
maybeReprocessPaymentSentMessage(reprocessOnError);
}, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount));
} else {
handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack
}
unlatchTrade();
})))
.executeTasks(true);
awaitTradeLatch();
@ -535,15 +587,28 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) {
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
if (!trade.isInitialized() || trade.isShutDown()) return;
log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer);
// validate signature
try {
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
} catch (Throwable t) {
log.warn("Ignoring PaymentReceivedMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage());
return;
}
// save message for reprocessing
trade.getSeller().setPaymentReceivedMessage(message);
trade.requestPersistence();
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> {
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
return;
}
synchronized (trade.getLock()) {
if (!trade.isInitialized() || trade.isShutDown()) return;
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
@ -649,48 +714,84 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
private void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
// handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
if (trade.getTradePeer(sender) == trade.getSeller()) {
processModel.setPaymentSentAckMessage(ackMessage);
trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
processModel.getTradeManager().requestPersistence();
} else if (trade.getTradePeer(sender) == trade.getArbitrator()) {
processModel.setPaymentSentAckMessageArbitrator(ackMessage);
} else if (!ackMessage.isSuccess()) {
String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() + " from "+ sender + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage();
log.warn(err);
return; // log error and ignore nack if not seller
}
// ignore if trade is completely finished
if (trade.isFinished()) return;
// get trade peer
TradePeer peer = trade.getTradePeer(sender);
if (peer == null) {
if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator();
else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker();
else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker();
}
if (peer == null) {
if (ackMessage.isSuccess()) log.warn("Received AckMessage from unknown peer for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid());
else log.warn("Received AckMessage with error state from unknown peer for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage());
return;
}
if (ackMessage.isSuccess()) {
log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid());
// update sender's node address
if (!peer.getNodeAddress().equals(sender)) {
log.info("Updating peer's node address from {} to {} using ACK message to {}", peer.getNodeAddress(), sender, ackMessage.getSourceMsgClassName());
peer.setNodeAddress(sender);
}
// handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time
if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) {
TradePeer peer = trade.getTradePeer(sender);
if (peer == null) {
// get the applicable peer based on the sourceUid
if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator();
else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker();
else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker();
}
if (peer == null) log.warn("Received AckMesage for DepositsConfirmedMessage for unknown peer: " + sender);
else peer.setDepositsConfirmedMessageAcked(true);
}
} else {
log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage());
// set trade state on deposit request nack
if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) {
// set trade state on deposit request nack
if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) {
if (!ackMessage.isSuccess()) {
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
processModel.getTradeManager().requestPersistence();
}
}
// handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time
if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) {
peer.setDepositsConfirmedAckMessage(ackMessage);
processModel.getTradeManager().requestPersistence();
}
// handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
if (trade.getTradePeer(sender) == trade.getSeller()) {
trade.getSeller().setPaymentSentAckMessage(ackMessage);
if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG);
processModel.getTradeManager().requestPersistence();
} else if (trade.getTradePeer(sender) == trade.getArbitrator()) {
trade.getArbitrator().setPaymentSentAckMessage(ackMessage);
processModel.getTradeManager().requestPersistence();
} else {
log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage());
return;
}
}
// handle ack for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time
if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) {
if (trade.getTradePeer(sender) == trade.getBuyer()) {
trade.getBuyer().setPaymentReceivedAckMessage(ackMessage);
if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG);
else trade.setState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG);
processModel.getTradeManager().requestPersistence();
} else if (trade.getTradePeer(sender) == trade.getArbitrator()) {
trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage);
processModel.getTradeManager().requestPersistence();
} else {
log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage());
return;
}
}
// generic handling
if (ackMessage.isSuccess()) {
log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid());
} else {
log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage());
handleError(ackMessage.getErrorMessage());
}
// notify trade listeners
trade.onAckMessage(ackMessage, sender);
}
protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) {

View File

@ -44,7 +44,6 @@ import java.util.UUID;
@Slf4j
public class ArbitratorProcessDepositRequest extends TradeTask {
private Throwable error;
private boolean depositResponsesSent;
@SuppressWarnings({"unused"})
@ -68,7 +67,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
processDepositRequest();
complete();
} catch (Throwable t) {
this.error = t;
trade.getProcessModel().error = t;
log.error("Error processing deposit request for trade {}: {}\n", trade.getId(), t.getMessage(), t);
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
failed(t);
@ -188,7 +187,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
trade.stateProperty().addListener((obs, oldState, newState) -> {
if (oldState == newState) return;
if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) {
sendDepositResponsesOnce(error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : error.getMessage());
sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage());
} else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) {
sendDepositResponsesOnce(null);
}
@ -230,7 +229,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
}
private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) {
log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), error);
log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), trade.getProcessModel().error);
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() {
@Override
public void onArrived() {

View File

@ -142,26 +142,26 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
@Override
protected void setStateSent() {
if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
getReceiver().setPaymentSentMessageState(MessageState.SENT);
tryToSendAgainLater();
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG);
getReceiver().setPaymentSentMessageState(MessageState.ARRIVED);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG);
getReceiver().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateFault() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG);
getReceiver().setPaymentSentMessageState(MessageState.FAILED);
processModel.getTradeManager().requestPersistence();
}
@ -170,7 +170,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
timer.stop();
}
if (listener != null) {
processModel.getPaymentSentMessageStateProperty().removeListener(listener);
trade.getSeller().getPaymentReceivedMessageStateProperty().removeListener(listener);
}
}
@ -185,7 +185,6 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
return;
}
log.info("We will send the message again to the peer after a delay of {} min.", delayInMin);
if (timer != null) {
timer.stop();
}
@ -194,8 +193,8 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
if (resendCounter == 0) {
listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue);
processModel.getPaymentSentMessageStateProperty().addListener(listener);
onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get());
getReceiver().getPaymentSentMessageStateProperty().addListener(listener);
onMessageStateChange(getReceiver().getPaymentSentMessageStateProperty().get());
}
// first re-send is after 2 minutes, then increase the delay exponentially
@ -212,12 +211,12 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask
}
private void onMessageStateChange(MessageState newValue) {
if (newValue == MessageState.ACKNOWLEDGED) {
trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
processModel.getTradeManager().requestPersistence();
if (isAckedByReceiver()) {
cleanup();
}
}
protected abstract boolean isAckedByReceiver();
protected boolean isAckedByReceiver() {
return getReceiver().isPaymentSentMessageAcked();
}
}

View File

@ -18,7 +18,6 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.network.MessageState;
import haveno.core.trade.Trade;
import haveno.core.trade.protocol.TradePeer;
import lombok.EqualsAndHashCode;
@ -39,26 +38,7 @@ public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSen
@Override
protected void setStateSent() {
super.setStateSent();
complete(); // don't wait for message to arbitrator
}
@Override
protected void setStateFault() {
// state only updated on seller message
}
@Override
protected void setStateStoredInMailbox() {
// state only updated on seller message
}
@Override
protected void setStateArrived() {
// state only updated on seller message
}
@Override
protected boolean isAckedByReceiver() {
return trade.getProcessModel().getPaymentSentMessageStatePropertyArbitrator().get() == MessageState.ACKNOWLEDGED;
}
}

View File

@ -18,7 +18,6 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.network.MessageState;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.TradeMessage;
import haveno.core.trade.protocol.TradePeer;
@ -40,25 +39,25 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes
@Override
protected void setStateSent() {
trade.getProcessModel().setPaymentSentMessageState(MessageState.SENT);
if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
super.setStateSent();
}
@Override
protected void setStateArrived() {
trade.getProcessModel().setPaymentSentMessageState(MessageState.ARRIVED);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG);
super.setStateArrived();
}
@Override
protected void setStateStoredInMailbox() {
trade.getProcessModel().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG);
super.setStateStoredInMailbox();
}
@Override
protected void setStateFault() {
trade.getProcessModel().setPaymentSentMessageState(MessageState.FAILED);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG);
super.setStateFault();
}
@ -69,9 +68,4 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
complete();
}
@Override
protected boolean isAckedByReceiver() {
return trade.getState().ordinal() >= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal();
}
}

View File

@ -87,7 +87,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
Integer subaddressIndex = null;
boolean reserveExactAmount = false;
if (trade instanceof MakerTrade) {
reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount();
reserveExactAmount = processModel.getOpenOfferManager().getOpenOffer(trade.getId()).get().isReserveExactAmount();
if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
}

View File

@ -18,7 +18,6 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.ThreadUtils;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.DepositsConfirmedMessage;
@ -63,17 +62,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
// update multisig hex
if (sender.getUpdatedMultisigHex() == null) {
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// try to import multisig hex (retry later)
if (!trade.isPayoutPublished()) {
ThreadUtils.submitToPool(() -> {
try {
trade.importMultisigHex();
} catch (Exception e) {
log.warn("Error importing multisig hex on deposits confirmed for trade " + trade.getId() + ": " + e.getMessage() + "\n", e);
}
});
}
trade.scheduleImportMultisigHex();
}
// persist

View File

@ -80,9 +80,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
return;
}
// save message for reprocessing
trade.getSeller().setPaymentReceivedMessage(message);
// set state
trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness());
@ -128,14 +125,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
private void processPayoutTx(PaymentReceivedMessage message) {
// adapt from 1.0.6 to 1.0.7 which changes field usage
// TODO: remove after future updates to allow old trades to clear
if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex());
trade.setPayoutTxHex(null);
}
// update wallet
trade.importMultisigHex();
trade.syncAndPollWallet();

View File

@ -48,7 +48,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress());
// update state from message
trade.getBuyer().setPaymentSentMessage(message);
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
String counterCurrencyTxId = message.getCounterCurrencyTxId();

View File

@ -43,13 +43,6 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
// handle first time preparation
if (trade.getArbitrator().getPaymentReceivedMessage() == null) {
// adapt from 1.0.6 to 1.0.7 which changes field usage
// TODO: remove after future updates to allow old trades to clear
if (trade.getPayoutTxHex() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) {
log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId());
trade.setPayoutTxHex(null);
}
// synchronize on lock for wallet operations
synchronized (trade.getWalletLock()) {
synchronized (HavenoUtils.getWalletFunctionLock()) {

View File

@ -35,11 +35,15 @@
package haveno.core.trade.protocol.tasks;
import com.google.common.base.Charsets;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.crypto.PubKeyRing;
import haveno.common.crypto.Sig;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.sign.SignedWitness;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.network.MessageState;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.PaymentReceivedMessage;
@ -47,15 +51,23 @@ import haveno.core.trade.messages.TradeMailboxMessage;
import haveno.core.trade.protocol.TradePeer;
import haveno.core.util.JsonUtil;
import haveno.network.p2p.NodeAddress;
import javafx.beans.value.ChangeListener;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.concurrent.TimeUnit;
@Slf4j
@EqualsAndHashCode(callSuper = true)
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
SignedWitness signedWitness = null;
private SignedWitness signedWitness = null;
private ChangeListener<MessageState> listener;
private Timer timer;
private static final int MAX_RESEND_ATTEMPTS = 20;
private int delayInMin = 10;
private int resendCounter = 0;
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
@ -77,6 +89,13 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
protected void run() {
try {
runInterceptHook();
// skip if already received
if (isReceived()) {
if (!isCompleted()) complete();
return;
}
super.run();
} catch (Throwable t) {
failed(t);
@ -134,29 +153,85 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
@Override
protected void setStateSent() {
trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
getReceiver().setPaymentReceivedMessageState(MessageState.SENT);
tryToSendAgainLater();
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateFault() {
trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG);
log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
getReceiver().setPaymentReceivedMessageState(MessageState.FAILED);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG);
log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
getReceiver().setPaymentReceivedMessageState(MessageState.STORED_IN_MAILBOX);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG);
log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
getReceiver().setPaymentReceivedMessageState(MessageState.ARRIVED);
processModel.getTradeManager().requestPersistence();
}
private void cleanup() {
if (timer != null) {
timer.stop();
}
if (listener != null) {
trade.getBuyer().getPaymentReceivedMessageStateProperty().removeListener(listener);
}
}
private void tryToSendAgainLater() {
// skip if already received
if (isReceived()) return;
if (resendCounter >= MAX_RESEND_ATTEMPTS) {
cleanup();
log.warn("We never received an ACK message when sending the PaymentReceivedMessage to the peer. We stop trying to send the message.");
return;
}
if (timer != null) {
timer.stop();
}
timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES);
if (resendCounter == 0) {
listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue);
getReceiver().getPaymentReceivedMessageStateProperty().addListener(listener);
onMessageStateChange(getReceiver().getPaymentReceivedMessageStateProperty().get());
}
// first re-send is after 2 minutes, then increase the delay exponentially
if (resendCounter == 0) {
int shortDelay = 2;
log.info("We will send the message again to the peer after a delay of {} min.", shortDelay);
timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES);
} else {
log.info("We will send the message again to the peer after a delay of {} min.", delayInMin);
timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES);
delayInMin = (int) ((double) delayInMin * 1.5);
}
resendCounter++;
}
private void onMessageStateChange(MessageState newValue) {
if (isReceived()) {
cleanup();
}
}
protected boolean isReceived() {
return getReceiver().isPaymentReceivedMessageReceived();
}
}

View File

@ -37,6 +37,30 @@ public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentRe
return trade.getBuyer();
}
@Override
protected void setStateSent() {
trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
super.setStateSent();
}
@Override
protected void setStateFault() {
trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG);
super.setStateFault();
}
@Override
protected void setStateStoredInMailbox() {
trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG);
super.setStateStoredInMailbox();
}
@Override
protected void setStateArrived() {
trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG);
super.setStateArrived();
}
// continue execution on fault so payment received message is sent to arbitrator
@Override
protected void onFault(String errorMessage, TradeMessage message) {

View File

@ -23,6 +23,7 @@ import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.crypto.PubKeyRing;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.network.MessageState;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.messages.DepositsConfirmedMessage;
@ -52,8 +53,8 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
try {
runInterceptHook();
// skip if already acked by receiver
if (isAckedByReceiver()) {
// skip if already acked or payout published
if (isAckedByReceiver() || trade.isPayoutPublished()) {
if (!isCompleted()) complete();
return;
}
@ -64,11 +65,17 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
}
}
@Override
protected abstract NodeAddress getReceiverNodeAddress();
protected abstract TradePeer getReceiver();
@Override
protected abstract PubKeyRing getReceiverPubKeyRing();
protected NodeAddress getReceiverNodeAddress() {
return getReceiver().getNodeAddress();
}
@Override
protected PubKeyRing getReceiverPubKeyRing() {
return getReceiver().getPubKeyRing();
}
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
@ -97,23 +104,24 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
@Override
protected void setStateSent() {
getReceiver().setDepositsConfirmedMessageState(MessageState.SENT);
tryToSendAgainLater();
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
// no additional handling
getReceiver().setDepositsConfirmedMessageState(MessageState.ARRIVED);
}
@Override
protected void setStateStoredInMailbox() {
// no additional handling
getReceiver().setDepositsConfirmedMessageState(MessageState.STORED_IN_MAILBOX);
}
@Override
protected void setStateFault() {
// no additional handling
getReceiver().setDepositsConfirmedMessageState(MessageState.FAILED);
}
private void cleanup() {
@ -151,7 +159,6 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
}
private boolean isAckedByReceiver() {
TradePeer peer = trade.getTradePeer(getReceiverNodeAddress());
return peer.isDepositsConfirmedMessageAcked();
return getReceiver().isDepositsConfirmedMessageAcked();
}
}

View File

@ -17,10 +17,9 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.crypto.PubKeyRing;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import haveno.network.p2p.NodeAddress;
import haveno.core.trade.protocol.TradePeer;
import lombok.extern.slf4j.Slf4j;
/**
@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfir
}
@Override
public NodeAddress getReceiverNodeAddress() {
return trade.getArbitrator().getNodeAddress();
}
@Override
public PubKeyRing getReceiverPubKeyRing() {
return trade.getArbitrator().getPubKeyRing();
protected TradePeer getReceiver() {
return trade.getArbitrator();
}
}

View File

@ -17,10 +17,9 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.crypto.PubKeyRing;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import haveno.network.p2p.NodeAddress;
import haveno.core.trade.protocol.TradePeer;
import lombok.extern.slf4j.Slf4j;
/**
@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMe
}
@Override
public NodeAddress getReceiverNodeAddress() {
return trade.getBuyer().getNodeAddress();
}
@Override
public PubKeyRing getReceiverPubKeyRing() {
return trade.getBuyer().getPubKeyRing();
protected TradePeer getReceiver() {
return trade.getBuyer();
}
}

View File

@ -17,10 +17,9 @@
package haveno.core.trade.protocol.tasks;
import haveno.common.crypto.PubKeyRing;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import haveno.network.p2p.NodeAddress;
import haveno.core.trade.protocol.TradePeer;
import lombok.extern.slf4j.Slf4j;
/**
@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedM
}
@Override
public NodeAddress getReceiverNodeAddress() {
return trade.getSeller().getNodeAddress();
}
@Override
public PubKeyRing getReceiverPubKeyRing() {
return trade.getSeller().getPubKeyRing();
protected TradePeer getReceiver() {
return trade.getSeller();
}
}

View File

@ -70,6 +70,9 @@ public class TakerReserveTradeFunds extends TradeTask {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (IllegalStateException e) {
log.warn("Illegal state creating reserve tx, offerId={}, error={}", trade.getShortId(), i + 1, e.getMessage());
throw e;
} catch (Exception e) {
log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);

View File

@ -135,7 +135,7 @@ public class MoneroWalletRpcManager {
// stop process
String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid());
log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", path, port, pid);
log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}, force={}", path, port, pid, force);
walletRpc.stopProcess(force);
}

View File

@ -30,6 +30,7 @@ public class Restrictions {
public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5;
public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1);
public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1);
public static int MAX_EXTRA_INFO_LENGTH = 1500;
// At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the
// mediated payout. For Refund agent cases we do not have that restriction.

View File

@ -767,7 +767,7 @@ public class XmrWalletService extends XmrWalletBase {
BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight());
double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue();
if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff);
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff);
log.info("Trade miner fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff);
// verify proof to fee address
BigInteger actualTradeFee = BigInteger.ZERO;
@ -785,7 +785,7 @@ public class XmrWalletService extends XmrWalletBase {
// verify trade fee amount
if (!actualTradeFee.equals(tradeFeeAmount)) {
if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) {
log.warn("Trade tx fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee);
log.warn("Trade fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee);
} else {
throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee);
}
@ -1016,6 +1016,13 @@ public class XmrWalletService extends XmrWalletBase {
public synchronized void resetAddressEntriesForOpenOffer(String offerId) {
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
// skip if failed trade is scheduled for processing // TODO: do not call this function in this case?
if (tradeManager.hasFailedScheduledTrade(offerId)) {
log.warn("Refusing to reset address entries because trade is scheduled for deletion with offerId={}", offerId);
return;
}
swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
// swap trade payout to available if applicable
@ -1164,7 +1171,7 @@ public class XmrWalletService extends XmrWalletBase {
public Stream<XmrAddressEntry> getAddressEntriesForAvailableBalanceStream() {
Stream<XmrAddressEntry> available = getFundedAvailableAddressEntries().stream();
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream());
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent()));
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent()));
available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked()));
return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0);
}

View File

@ -495,6 +495,7 @@ createOffer.triggerPrice.tooltip=As protection against drastic price movements y
deactivates the offer if the market price reaches that value.
createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0}
createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0}
createOffer.extraInfo.invalid.tooLong=Must not exceed {0} characters.
# new entries
createOffer.placeOfferButton=Review: Place offer to {0} monero

View File

@ -203,12 +203,12 @@ class GrpcOffersService extends OffersImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{
put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS));
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS));
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, SECONDS));
put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS));
put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS));
put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS));
put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
}}
)));
}

View File

@ -251,15 +251,15 @@ class GrpcTradesService extends TradesImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS));
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS));
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS));
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS));
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES));
put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
}}
)));
}

View File

@ -60,6 +60,6 @@
</content_rating>
<releases>
<release version="1.0.18" date="2025-01-19"/>
<release version="1.0.19" date="2025-03-10"/>
</releases>
</component>

View File

@ -5,10 +5,10 @@
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
<key>CFBundleVersion</key>
<string>1.0.18</string>
<string>1.0.19</string>
<key>CFBundleShortVersionString</key>
<string>1.0.18</string>
<string>1.0.19</string>
<key>CFBundleExecutable</key>
<string>Haveno</string>

View File

@ -0,0 +1,140 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXTextArea;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.util.validation.JFXInputValidator;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Skin;
/**
* TextArea with validation support.
* If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is
* needed the validationResultProperty can be used for applying validation result done by external validation.
* In case the isValid property in validationResultProperty get set to false we display a red border and an error
* message within the errorMessageDisplay placed on the right of the text area.
* The errorMessageDisplay gets closed when the ValidatingTextArea instance gets removed from the scene graph or when
* hideErrorMessageDisplay() is called.
* There can be only 1 errorMessageDisplays at a time we use static field for it.
* The position is derived from the position of the textArea itself or if set from the layoutReference node.
*/
//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements.
public class InputTextArea extends JFXTextArea {
private final ObjectProperty<InputValidator.ValidationResult> validationResult = new SimpleObjectProperty<>
(new InputValidator.ValidationResult(true));
private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator();
private InputValidator validator;
private String errorMessage = null;
public InputValidator getValidator() {
return validator;
}
public void setValidator(InputValidator validator) {
this.validator = validator;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public InputTextArea() {
super();
getValidators().add(jfxValidationWrapper);
validationResult.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
jfxValidationWrapper.resetValidation();
if (!newValue.isValid) {
if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking
validate(); // ensure that the new error message replaces the old one
}
if (this.errorMessage != null) {
jfxValidationWrapper.applyErrorMessage(this.errorMessage);
} else {
jfxValidationWrapper.applyErrorMessage(newValue);
}
}
validate();
}
});
textProperty().addListener((o, oldValue, newValue) -> {
refreshValidation();
});
focusedProperty().addListener((o, oldValue, newValue) -> {
if (validator != null) {
if (!oldValue && newValue) {
this.validationResult.set(new InputValidator.ValidationResult(true));
} else {
this.validationResult.set(validator.validate(getText()));
}
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public void resetValidation() {
jfxValidationWrapper.resetValidation();
String input = getText();
if (input.isEmpty()) {
validationResult.set(new InputValidator.ValidationResult(true));
} else {
validationResult.set(validator.validate(input));
}
}
public void refreshValidation() {
if (validator != null) {
this.validationResult.set(validator.validate(getText()));
}
}
public void setInvalid(String message) {
validationResult.set(new InputValidator.ValidationResult(false, message));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public ObjectProperty<InputValidator.ValidationResult> validationResultProperty() {
return validationResult;
}
protected Skin<?> createDefaultSkin() {
return new JFXTextAreaSkinHavenoStyle(this);
}
}

View File

@ -501,15 +501,15 @@ tree-table-view:focused {
-jfx-default-color: -bs-color-primary;
}
.jfx-date-picker .jfx-text-field {
.jfx-date-picker .jfx-text-field .jfx-text-area {
-fx-padding: 0.333333em 0em 0.333333em 0em;
}
.jfx-date-picker .jfx-text-field > .input-line {
.jfx-date-picker .jfx-text-field .jfx-text-area > .input-line {
-fx-translate-x: 0em;
}
.jfx-date-picker .jfx-text-field > .input-focused-line {
.jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line {
-fx-translate-x: 0em;
}

View File

@ -39,10 +39,6 @@
-fx-image: url("../../images/remove.png");
}
#image-edit {
-fx-image: url("../../images/edit.png");
}
#image-buy-white {
-fx-image: url("../../images/buy_white.png");
}

View File

@ -420,7 +420,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
havenoSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show());
havenoSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig);
havenoSetup.setShowPopupIfInvalidXmrConfigHandler(this::showPopupIfInvalidXmrConfig);
havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> {
// We copy the array as we will mutate it later
@ -536,7 +536,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
});
}
private void showPopupIfInvalidBtcConfig() {
private void showPopupIfInvalidXmrConfig() {
preferences.setMoneroNodesOptionOrdinal(0);
new Popup().warning(Res.get("settings.net.warn.invalidXmrConfig"))
.hideCloseButton()

View File

@ -225,8 +225,8 @@ public class LockedView extends ActivatableView<VBox, Void> {
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(offerId);
if (tradeOptional.isPresent()) {
return Optional.of(tradeOptional.get());
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {
return Optional.of(openOfferManager.getOpenOfferById(offerId).get());
} else if (openOfferManager.getOpenOffer(offerId).isPresent()) {
return Optional.of(openOfferManager.getOpenOffer(offerId).get());
} else {
return Optional.empty();
}

View File

@ -224,8 +224,8 @@ public class ReservedView extends ActivatableView<VBox, Void> {
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(offerId);
if (tradeOptional.isPresent()) {
return Optional.of(tradeOptional.get());
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {
return Optional.of(openOfferManager.getOpenOfferById(offerId).get());
} else if (openOfferManager.getOpenOffer(offerId).isPresent()) {
return Optional.of(openOfferManager.getOpenOffer(offerId).get());
} else {
return Optional.<Tradable>empty();
}

View File

@ -44,8 +44,8 @@ import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.BalanceTextField;
import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.FundsTextField;
import haveno.desktop.components.HavenoTextArea;
import haveno.desktop.components.InfoInputTextField;
import haveno.desktop.components.InputTextArea;
import haveno.desktop.components.InputTextField;
import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.main.MainView;
@ -76,7 +76,6 @@ import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
@ -140,7 +139,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
private BalanceTextField balanceTextField;
private ToggleButton reserveExactAmountSlider;
private ToggleButton buyerAsTakerWithoutDepositSlider;
protected TextArea extraInfoTextArea;
protected InputTextArea extraInfoTextArea;
private FundsTextField totalToPayTextField;
private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel,
waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel,
@ -211,7 +210,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
createListeners();
balanceTextField.setFormatter(model.getBtcFormatter());
balanceTextField.setFormatter(model.getXmrFormatter());
paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter());
paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"),
@ -592,6 +591,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult);
volumeTextField.validationResultProperty().bind(model.volumeValidationResult);
securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult);
extraInfoTextArea.validationResultProperty().bind(model.extraInfoValidationResult);
// funding
fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed));
@ -713,7 +713,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
triggerPriceInputTextField.setText(model.triggerPrice.get());
};
extraInfoFocusedListener = (observable, oldValue, newValue) -> {
model.onFocusOutExtraInfoTextField(oldValue, newValue);
model.onFocusOutExtraInfoTextArea(oldValue, newValue);
extraInfoTextArea.setText(model.extraInfo.get());
};
@ -1097,7 +1097,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment);
GridPane.setColumnSpan(extraInfoTitledGroupBg, 3);
extraInfoTextArea = new HavenoTextArea();
extraInfoTextArea = new InputTextArea();
extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer"));
extraInfoTextArea.getStyleClass().add("text-area");
extraInfoTextArea.setWrapText(true);
@ -1109,7 +1109,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING);
GridPane.setColumnIndex(extraInfoTextArea, 0);
GridPane.setHalignment(extraInfoTextArea, HPos.LEFT);
GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 10, 0));
gridPane.getChildren().add(extraInfoTextArea);
}

View File

@ -99,7 +99,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation;
private final Preferences preferences;
protected final CoinFormatter btcFormatter;
protected final CoinFormatter xmrFormatter;
private final FiatVolumeValidator fiatVolumeValidator;
private final AmountValidator4Decimals amountValidator4Decimals;
private final AmountValidator8Decimals amountValidator8Decimals;
@ -160,6 +160,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
final ObjectProperty<InputValidator.ValidationResult> triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true));
final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> securityDepositValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> extraInfoValidationResult = new SimpleObjectProperty<>();
private ChangeListener<String> amountStringListener;
private ChangeListener<String> minAmountStringListener;
@ -195,26 +196,26 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
FiatVolumeValidator fiatVolumeValidator,
AmountValidator4Decimals amountValidator4Decimals,
AmountValidator8Decimals amountValidator8Decimals,
XmrValidator btcValidator,
XmrValidator xmrValidator,
SecurityDepositValidator securityDepositValidator,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
Preferences preferences,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter,
OfferUtil offerUtil) {
super(dataModel);
this.fiatVolumeValidator = fiatVolumeValidator;
this.amountValidator4Decimals = amountValidator4Decimals;
this.amountValidator8Decimals = amountValidator8Decimals;
this.xmrValidator = btcValidator;
this.xmrValidator = xmrValidator;
this.securityDepositValidator = securityDepositValidator;
this.priceFeedService = priceFeedService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.navigation = navigation;
this.preferences = preferences;
this.btcFormatter = btcFormatter;
this.xmrFormatter = xmrFormatter;
this.offerUtil = offerUtil;
paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId);
@ -500,11 +501,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
};
extraInfoStringListener = (ov, oldValue, newValue) -> {
if (newValue != null) {
extraInfo.set(newValue);
} else {
extraInfo.set("");
}
onExtraInfoTextAreaChanged();
};
isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState();
@ -531,7 +528,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
tradeFee.set(HavenoUtils.formatXmr(makerFee));
tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getMaxMakerFee(),
btcFormatter));
xmrFormatter));
}
@ -665,7 +662,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
createOfferRequested = false;
createOfferCanceled = true;
OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId());
Optional<OpenOffer> openOffer = openOfferManager.getOpenOffer(offer.getId());
if (openOffer.isPresent()) {
openOfferManager.cancelOpenOffer(openOffer.get(), () -> {
UserThread.execute(() -> {
@ -836,8 +833,16 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
}
public void onFocusOutExtraInfoTextField(boolean oldValue, boolean newValue) {
public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) {
if (oldValue && !newValue) {
onExtraInfoTextAreaChanged();
}
}
public void onExtraInfoTextAreaChanged() {
extraInfoValidationResult.set(getExtraInfoValidationResult());
updateButtonDisableState();
if (extraInfoValidationResult.get().isValid) {
dataModel.setExtraInfo(extraInfo.get());
}
}
@ -1045,8 +1050,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
.show();
}
CoinFormatter getBtcFormatter() {
return btcFormatter;
CoinFormatter getXmrFormatter() {
return xmrFormatter;
}
public boolean isShownAsBuyOffer() {
@ -1064,7 +1069,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
public String getTradeAmount() {
return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.getAmount().get(),
btcFormatter);
xmrFormatter);
}
public String getSecurityDepositLabel() {
@ -1084,7 +1089,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getSecurityDeposit(),
dataModel.getAmount().get(),
btcFormatter
xmrFormatter
);
}
@ -1097,7 +1102,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil,
dataModel.getMaxMakerFee(),
dataModel.getAmount().get(),
btcFormatter);
xmrFormatter);
}
public String getMakerFeePercentage() {
@ -1108,7 +1113,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
public String getTotalToPayInfo() {
return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil,
dataModel.totalToPay.get(),
btcFormatter);
xmrFormatter);
}
public String getFundsStructure() {
@ -1181,7 +1186,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private void setAmountToModel() {
if (amount.get() != null && !amount.get().isEmpty()) {
BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), btcFormatter));
BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter));
long maxTradeLimit = dataModel.getMaxTradeLimit();
Price price = dataModel.getPrice().get();
@ -1202,7 +1207,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private void setMinAmountToModel() {
if (minAmount.get() != null && !minAmount.get().isEmpty()) {
BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), btcFormatter));
BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter));
Price price = dataModel.getPrice().get();
long maxTradeLimit = dataModel.getMaxTradeLimit();
@ -1343,10 +1348,20 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid;
}
inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid;
isNextButtonDisabled.set(!inputDataValid);
isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get());
}
private ValidationResult getExtraInfoValidationResult() {
if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) {
return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.MAX_EXTRA_INFO_LENGTH));
} else {
return new InputValidator.ValidationResult(true);
}
}
private void updateMarketPriceToManual() {
final String currencyCode = dataModel.getTradeCurrencyCode().get();
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);

View File

@ -20,6 +20,7 @@ package haveno.desktop.main.offer.offerbook;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView;
import haveno.common.UserThread;
import haveno.common.app.DevEnv;
import haveno.common.util.Tuple3;
@ -1082,7 +1083,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
button.setPrefWidth(10000);
}
final ImageView iconView2 = new ImageView();
MaterialDesignIconView iconView2 = new MaterialDesignIconView(MaterialDesignIcon.PENCIL);
final AutoTooltipButton button2 = new AutoTooltipButton();
{
@ -1135,7 +1136,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
title = Res.get("shared.remove");
button.setOnAction(e -> onRemoveOpenOffer(offer));
iconView2.setId("image-edit");
iconView2.setSize("16px");
button2.updateText(Res.get("shared.edit"));
button2.setOnAction(e -> onEditOpenOffer(offer));
button2.setManaged(true);

View File

@ -711,6 +711,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
abstract String getCurrencyCodeFromPreferences(OfferDirection direction);
public OpenOffer getOpenOffer(Offer offer) {
return openOfferManager.getOpenOfferById(offer.getId()).orElse(null);
return openOfferManager.getOpenOffer(offer.getId()).orElse(null);
}
}

View File

@ -284,6 +284,7 @@ class TakeOfferDataModel extends OfferDataModel {
// handle error
if (errorMsg != null) {
new Popup().warning(errorMsg).show();
log.warn("Error taking offer " + offer.getId() + ": " + errorMsg);
errorMessageHandler.handleErrorMessage(errorMsg);
}
}

View File

@ -765,9 +765,7 @@ public abstract class Overlay<T extends Overlay<T>> {
FormBuilder.getIconForLabel(AwesomeIcon.COPY, copyIcon, "1.1em");
copyIcon.addEventHandler(MOUSE_CLICKED, mouseEvent -> {
if (message != null) {
String forClipboard = headLineLabel.getText() + System.lineSeparator() + message
+ System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString());
Utilities.copyToClipboard(forClipboard);
Utilities.copyToClipboard(getClipboardText());
Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard"));
Node node = (Node) mouseEvent.getSource();
UserThread.runAfter(() -> tp.hide(), 1);
@ -1083,6 +1081,11 @@ public abstract class Overlay<T extends Overlay<T>> {
return isDisplayed;
}
public String getClipboardText() {
return headLineLabel.getText() + System.lineSeparator() + message
+ System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString());
}
@Override
public String toString() {
return "Popup{" +

View File

@ -342,7 +342,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null;
// get offer challenge
OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOfferById(offer.getId()).orElse(null);
OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOffer(offer.getId()).orElse(null);
String offerChallenge = myOpenOffer == null ? null : myOpenOffer.getChallenge();
rows = 3;

View File

@ -37,10 +37,10 @@ import java.io.ByteArrayInputStream;
public class QRCodeWindow extends Overlay<QRCodeWindow> {
private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class);
private final ImageView qrCodeImageView;
private final String bitcoinURI;
private final String moneroUri;
public QRCodeWindow(String bitcoinURI) {
this.bitcoinURI = bitcoinURI;
this.moneroUri = bitcoinURI;
final byte[] imageBytes = QRCode
.from(bitcoinURI)
.withSize(300, 300)
@ -70,7 +70,7 @@ public class QRCodeWindow extends Overlay<QRCodeWindow> {
GridPane.setHalignment(qrCodeImageView, HPos.CENTER);
gridPane.getChildren().add(qrCodeImageView);
String request = bitcoinURI.replace("%20", " ").replace("?", "\n?").replace("&", "\n&");
String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&");
Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request));
infoLabel.setMouseTransparent(true);
infoLabel.setWrapText(true);
@ -87,4 +87,8 @@ public class QRCodeWindow extends Overlay<QRCodeWindow> {
applyStyles();
display();
}
public String getClipboardText() {
return moneroUri;
}
}

View File

@ -553,13 +553,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
disputeManager = arbitrationManager;
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// send dispute opened message
sendDisputeOpenedMessage(dispute, disputeManager);
tradeManager.requestPersistence();

View File

@ -100,7 +100,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
private final ObjectProperty<BuyerState> buyerState = new SimpleObjectProperty<>();
private final ObjectProperty<SellerState> sellerState = new SimpleObjectProperty<>();
@Getter
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private final ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private Subscription tradeStateSubscription;
private Subscription paymentAccountDecryptedSubscription;
private Subscription payoutStateSubscription;
@ -186,7 +186,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
if (messageStateSubscription != null) {
messageStateSubscription.unsubscribe();
messageStateProperty.set(MessageState.UNDEFINED);
paymentSentMessageStateProperty.set(MessageState.UNDEFINED);
}
if (selectedItem != null) {
@ -200,7 +200,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
onPayoutStateChanged(state);
});
messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStateProperty(), this::onMessageStateChanged);
messageStateSubscription = EasyBind.subscribe(trade.getSeller().getPaymentSentMessageStateProperty(), this::onPaymentSentMessageStateChanged);
}
}
}
@ -215,8 +215,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
});
}
private void onMessageStateChanged(MessageState messageState) {
messageStateProperty.set(messageState);
private void onPaymentSentMessageStateChanged(MessageState messageState) {
paymentSentMessageStateProperty.set(messageState);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -416,15 +416,20 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
// payment received
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
if (trade instanceof BuyerTrade) buyerState.set(BuyerState.STEP4);
else if (trade instanceof SellerTrade) sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
if (trade instanceof BuyerTrade) {
buyerState.set(BuyerState.UNDEFINED); // TODO: resetting screen to populate summary information which can be missing before payout message processed
buyerState.set(BuyerState.STEP4);
} else if (trade instanceof SellerTrade) {
sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
}
break;
// seller step 3
// seller step 3 or 4 if published
case SELLER_CONFIRMED_PAYMENT_RECEIPT:
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
case BUYER_RECEIVED_PAYMENT_RECEIVED_MSG:
sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
break;

View File

@ -52,7 +52,7 @@ public class BuyerStep3View extends TradeStepView {
public void activate() {
super.activate();
model.getMessageStateProperty().addListener(messageStateChangeListener);
model.getPaymentSentMessageStateProperty().addListener(messageStateChangeListener);
updateMessageStateInfo();
}
@ -60,7 +60,7 @@ public class BuyerStep3View extends TradeStepView {
public void deactivate() {
super.deactivate();
model.getMessageStateProperty().removeListener(messageStateChangeListener);
model.getPaymentSentMessageStateProperty().removeListener(messageStateChangeListener);
}
@ -87,7 +87,7 @@ public class BuyerStep3View extends TradeStepView {
}
private void updateMessageStateInfo() {
MessageState messageState = model.getMessageStateProperty().get();
MessageState messageState = model.getPaymentSentMessageStateProperty().get();
textFieldWithIcon.setText(Res.get("message.state." + messageState.name()));
Label iconLabel = textFieldWithIcon.getIconLabel();
switch (messageState) {

View File

@ -79,7 +79,7 @@ public class BuyerStep4View extends TradeStepView {
TitledGroupBg completedTradeLabel = new TitledGroupBg();
if (trade.getDisputeState().isMediated()) {
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.mediated"));
} else if (trade.getDisputeState().isArbitrated() && trade.getDisputes().get(0).getDisputeResultProperty().get() != null) {
} else if (trade.getDisputeState().isArbitrated() && trade.getDisputeResult() != null) {
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle.arbitrated"));
} else {
completedTradeLabel.setText(Res.get("portfolio.pending.step5_buyer.groupTitle"));
@ -90,8 +90,8 @@ public class BuyerStep4View extends TradeStepView {
gridPane.getChildren().add(hBox2);
GridPane.setRowSpan(hBox2, 5);
if (trade.getDisputeState().isNotDisputed()) {
addCompactTopLabelTextField(gridPane, gridRow, getBtcTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE);
if (trade.isPaymentReceived()) {
addCompactTopLabelTextField(gridPane, gridRow, getXmrTradeAmountLabel(), model.getTradeVolume(), Layout.TWICE_FIRST_ROW_DISTANCE);
addCompactTopLabelTextField(gridPane, ++gridRow, getTraditionalTradeAmountLabel(), model.getFiatVolume());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.refunded"), model.getSecurityDeposit());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.tradeFee"), model.getTradeFee());
@ -149,7 +149,7 @@ public class BuyerStep4View extends TradeStepView {
}
}
protected String getBtcTradeAmountLabel() {
protected String getXmrTradeAmountLabel() {
return Res.get("portfolio.pending.step5_buyer.bought");
}

View File

@ -135,6 +135,7 @@ public class SellerStep3View extends TradeStepView {
statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
case BUYER_RECEIVED_PAYMENT_RECEIVED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived"));
break;

View File

@ -32,7 +32,7 @@ public class SellerStep4View extends BuyerStep4View {
}
@Override
protected String getBtcTradeAmountLabel() {
protected String getXmrTradeAmountLabel() {
return Res.get("portfolio.pending.step5_seller.sold");
}

View File

@ -270,7 +270,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master
<b>Set the mandatory minimum version for trading (optional)</b>
If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.18) in the field labeled "Min. version required for trading".
If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.19) in the field labeled "Min. version required for trading".
<b>Send update alert</b>

View File

@ -1,10 +1,16 @@
## Importing Haveno into development environment
# Importing Haveno dev environment
This document describes how to import Haveno into an integrated development environment (IDE).
## Importing Haveno into Eclipse IDE
First [install and run a Haveno test network](installing.md), then use the following instructions to import Haveno into an IDE.
These steps describe how to import Haveno into Eclipse IDE for development. You can also develop using [IntelliJ IDEA](#importing-haveno-into-intellij-idea) or VSCode if you prefer.
## Visual Studio Code (recommended)
1. Download and open Visual Studio Code: https://code.visualstudio.com/.
2. File > Add folder to Workspace...
3. Browse to the `haveno` git project.
## Eclipse IDE
> Note: Use default values unless specified otherwise.
@ -26,7 +32,7 @@ These steps describe how to import Haveno into Eclipse IDE for development. You
You are now ready to make, run, and test changes to the Haveno project!
## Importing Haveno into IntelliJ IDEA
## IntelliJ IDEA
> Note: These instructions are outdated and for Haveno.

View File

@ -10,7 +10,7 @@ As per the project's authors, `netlayer` is _"essentially a wrapper around the o
easy use and convenient integration into Kotlin/Java projects"_.
Similarly, `tor-binary` is _"[the] Tor binary packaged in a way that can be used for java projects"_. The project
unpacks the tor browser binaries to extract and repackage the tor binaries themselves.
unpacks the Tor Browser binaries to extract and repackage the tor binaries themselves.
Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts.
@ -22,8 +22,8 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts.
- Find out which tor version Haveno currently uses
- Find out the current `netlayer` version (see `netlayerVersion` in `haveno/build.gradle`)
- Find that release on the project's [releases page][3]
- The release description says which tor version it includes
- Find that tag on the project's [Tags page][3]
- The tag description says which tor version it includes
- Find out the latest available tor release
- See the [official tor changelog][4]
@ -32,23 +32,24 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts.
During this update, you will need to keep track of:
- the new tor browser version
- the new Tor Browser version
- the new tor binary version
Create a PR for the `master` branch of [tor-binary][2] with the following changes:
- Decide which tor browser version contains the desired tor binary version
- The official tor browser releases are here: https://dist.torproject.org/torbrowser/
- For the chosen tor browser version, get the list of SHA256 checksums and its signature
- For example, for tor browser 10.0.12:
- https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt
- https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt.asc
- Decide which Tor Browser version contains the desired tor binary version
- The latest official Tor Browser releases are here: https://dist.torproject.org/torbrowser/
- All official Tor Browser releases are here: https://archive.torproject.org/tor-package-archive/torbrowser/
- For the chosen Tor Browser version, get the list of SHA256 checksums and its signature
- For example, for Tor Browser 14.0.7:
- https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt
- https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt.asc
- Verify the signature of the checksums list (see [instructions][5])
- Update the `tor-binary` checksums
- For each file present in `tor-binary/tor-binary-resources/checksums`:
- Rename the file such that it reflects the new tor browser version, but preserves the naming scheme
- Rename the file such that it reflects the new Tor Browser version, but preserves the naming scheme
- Update the contents of the file with the corresponding SHA256 checksum from the list
- Update `torbrowser.version` to the new tor browser version in:
- Update `torbrowser.version` to the new Tor Browser version in:
- `tor-binary/build.xml`
- `tor-binary/pom.xml`
- Update `version` to the new tor binary version in:
@ -72,7 +73,7 @@ next.
### 3. Update `netlayer`
Create a PR for the `externaltor` branch of [netlayer][1] with the following changes:
Create a PR for the `master` branch of [netlayer][1] with the following changes:
- In `netlayer/pom.xml`:
- Update `tor-binary.version` to the `tor-binary` commit ID from above (e.g. `a4b868a`)
@ -82,13 +83,13 @@ Create a PR for the `externaltor` branch of [netlayer][1] with the following cha
- `netlayer/tor.external/pom.xml`
- `netlayer/tor.native/pom.xml`
Once the PR is merged, make a note of the commit ID in the `externaltor` branch (for example `32779ac`), as it will be
Once the PR is merged, make a note of the commit ID in the `master` branch (for example `32779ac`), as it will be
needed next.
Create a tag for the new artefact version, having the new tor binary version as description, for example:
```
# Create tag locally for new netlayer release, on the externaltor branch
# Create tag locally for new netlayer release, on the master branch
git tag -s 0.7.0 -m"tor 0.4.5.6"
# Push it to netlayer repo
@ -105,8 +106,6 @@ Create a Haveno PR with the following changes:
- See instructions in `haveno/gradle/witness/gradle-witness.gradle`
## Credits
Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for their work on the original
@ -115,8 +114,8 @@ Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for thei
[1]: https://github.com/bisq-network/netlayer "netlayer"
[2]: https://github.com/bisq-network/tor-binary "tor-binary"
[3]: https://github.com/bisq-network/netlayer/releases "netlayer releases"
[1]: https://github.com/haveno-dex/netlayer "netlayer"
[2]: https://github.com/haveno-dex/tor-binary "tor-binary"
[3]: https://github.com/haveno-dex/netlayer/tags "netlayer Tags"
[4]: https://gitweb.torproject.org/tor.git/plain/ChangeLog "tor changelog"
[5]: https://support.torproject.org/tbb/how-to-verify-signature/ "verify tor signature"

View File

@ -205,44 +205,49 @@
<sha256 value="d4ea711258c783e0accb8feaaa204f0414781551b0159fa17e5f1869200f96f7" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.netlayer" name="tor" version="700ec94f0f">
<artifact name="tor-700ec94f0f.jar">
<sha256 value="63eafc2bf43ae2556d5a24f23b5ddfe371c1ac01b7bc595d6fdb7eadcba37d52" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.netlayer" name="tor" version="d9c60be46d">
<artifact name="tor-d9c60be46d.jar">
<sha256 value="e35f3bf160f96a99255ba3c408f096e65c0092658c5fac182561e081d97c4638" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.netlayer" name="tor.external" version="700ec94f0f">
<artifact name="tor.external-700ec94f0f.jar">
<sha256 value="01b506ec84697a08abfad2ab1928a8ba8bda36f588f05fbb14e5b9cacdbd0e3d" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.netlayer" name="tor.external" version="d9c60be46d">
<artifact name="tor.external-d9c60be46d.jar">
<sha256 value="2cb926847283dea14acb732aa8df8042c59a5a803b1a80e171abbdeea44a7eb0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.netlayer" name="tor.native" version="700ec94f0f">
<artifact name="tor.native-700ec94f0f.jar">
<sha256 value="51886b73f9c41d1d16ab7995af9846f67d6f7942def0c182b56a2a801ae301d3" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.netlayer" name="tor.native" version="d9c60be46d">
<artifact name="tor.native-d9c60be46d.jar">
<sha256 value="2cc31cd9aed3d3368a0fdeaa529d7fa83f732d170f331ec471a37a45c30c80cb" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-geoip" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
<artifact name="tor-binary-geoip-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
<sha256 value="7bd0fa5e818825d8f0c87a52cc0c468a06fd7850c825b9b36ba82d7a3d9f2fa5" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-geoip" version="2c02f6b133da79134312b964f2d0f9630c7dfa67">
<artifact name="tor-binary-geoip-2c02f6b133da79134312b964f2d0f9630c7dfa67.jar">
<sha256 value="98ba8e66de5198d5a558db0ec932faccb52c4eff9c50deefb6427c7f083be665" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux32" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
<artifact name="tor-binary-linux32-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
<sha256 value="4b4b3d822d8ad88f874450385751d0b26b41e2724d0d9b703acd9e4b73b3ba5d" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux32" version="2c02f6b133da79134312b964f2d0f9630c7dfa67">
<artifact name="tor-binary-linux32-2c02f6b133da79134312b964f2d0f9630c7dfa67.jar">
<sha256 value="f6a63ed5bdbc9417107c8425bb241ff841a0a8076492fbbcc11921e46bd360a0" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux64" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
<artifact name="tor-binary-linux64-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
<sha256 value="512b6d52217feed0efe84c1f43888fc8a8ba32a8998486c32e233a031dddbd94" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux64" version="2c02f6b133da79134312b964f2d0f9630c7dfa67">
<artifact name="tor-binary-linux64-2c02f6b133da79134312b964f2d0f9630c7dfa67.jar">
<sha256 value="ef055a43e73f3bc4fffb8fafcafe513566d8e019c9518fe2d1bd81bbcad9d537" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-macos" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
<artifact name="tor-binary-macos-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
<sha256 value="f68f9c6a3f56d084bd9426ff3834bcc90b07a4489357b04ef8151465a73c8783" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-linuxaarch64" version="2c02f6b133da79134312b964f2d0f9630c7dfa67">
<artifact name="tor-binary-linuxaarch64-2c02f6b133da79134312b964f2d0f9630c7dfa67.jar">
<sha256 value="652b95aad1aac64583c6d8e3b573853b6a6877b363b3040dc232eb0b14e78946" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-windows" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
<artifact name="tor-binary-windows-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
<sha256 value="94f7090f34dc6f12cdd5e5a247f7f0c4aeb40693258fd5365db41a2b8a71b197" origin="Generated by Gradle"/>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-macos" version="2c02f6b133da79134312b964f2d0f9630c7dfa67">
<artifact name="tor-binary-macos-2c02f6b133da79134312b964f2d0f9630c7dfa67.jar">
<sha256 value="a6815e420956ce1faf3ddeaea87ef08666f4744cd0cfd5912b414943b41d5eba" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.haveno-dex.tor-binary" name="tor-binary-windows" version="2c02f6b133da79134312b964f2d0f9630c7dfa67">
<artifact name="tor-binary-windows-2c02f6b133da79134312b964f2d0f9630c7dfa67.jar">
<sha256 value="9144e233fe350164a9ee0bdd42ea9b16db2d2ea486e5ec634ff040fd5934a609" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.github.johnrengelman" name="shadow" version="8.1.1">

View File

@ -95,7 +95,9 @@ public class Socks5ProxyProvider {
String[] tokens = socks5ProxyAddress.split(":");
if (tokens.length == 2) {
try {
return new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1]));
Socks5Proxy proxy = new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1]));
proxy.resolveAddrLocally(false);
return proxy;
} catch (UnknownHostException e) {
log.error(ExceptionUtils.getStackTrace(e));
}

View File

@ -385,7 +385,7 @@ message DisputeOpenedMessage {
NodeAddress sender_node_address = 2;
string uid = 3;
SupportType type = 4;
string updated_multisig_hex = 5;
string opener_updated_multisig_hex = 5;
PaymentSentMessage payment_sent_message = 6;
}
@ -1465,6 +1465,7 @@ message Trade {
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 24;
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25;
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 26;
BUYER_RECEIVED_PAYMENT_RECEIVED_MSG = 27;
}
enum Phase {
@ -1568,8 +1569,8 @@ message ProcessModel {
bytes payout_tx_signature = 4;
bool use_savings_wallet = 5;
int64 funds_needed_for_trade = 6;
string payment_sent_message_state = 7;
string payment_sent_message_state_arbitrator = 8;
string payment_sent_message_state_seller = 7 [deprecated = true];
string payment_sent_message_state_arbitrator = 8 [deprecated = true];
bytes maker_signature = 9;
TradePeer maker = 10;
TradePeer taker = 11;
@ -1581,6 +1582,7 @@ message ProcessModel {
int64 seller_payout_amount_from_mediation = 17;
int64 trade_protocol_error_height = 18;
string trade_fee_address = 19;
bool import_multisig_hex_scheduled = 20;
}
message TradePeer {
@ -1612,7 +1614,7 @@ message TradePeer {
string made_multisig_hex = 31;
string exchanged_multisig_hex = 32;
string updated_multisig_hex = 33;
bool deposits_confirmed_message_acked = 34;
bool deposits_confirmed_message_acked = 34 [deprecated = true];
string deposit_tx_hash = 35;
string deposit_tx_hex = 36;
string deposit_tx_key = 37;
@ -1621,6 +1623,9 @@ message TradePeer {
string unsigned_payout_tx_hex = 40;
int64 payout_tx_fee = 41;
int64 payout_amount = 42;
string deposits_confirmed_message_state = 43;
string payment_sent_message_state = 44;
string payment_received_message_state = 45;
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,401 @@
# Haveno on Qubes/Whonix
## **Conventions:**
+ \# Requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command
+ $ or % Requires given linux commands to be executed as a regular non-privileged user
+ \<VAR> Used to indicate user supplied variable
---
## **Installation - Scripted & Manual (GUI + CLI):**
### *Acquire release files:*
#### In `dispXXXX` AppVM:
##### Clone repository
```shell
% git clone --depth=1 https://github.com/haveno-dex/haveno
```
---
### **Create TemplateVM, NetVM & AppVM:**
#### Scripted
##### In `dispXXXX` AppVM:
###### Prepare files for transfer to `dom0`
```shell
% tar -C haveno/scripts/install_qubes/scripts/0-dom0 -zcvf /tmp/haveno.tgz .
```
##### In `dom0`:
###### Copy files to `dom0`
```shell
$ mkdir -p /tmp/haveno && qvm-run -p dispXXXX 'cat /tmp/haveno.tgz' > /tmp/haveno.tgz && tar -C /tmp/haveno -zxfv /tmp/haveno.tgz
$ bash /tmp/haveno/0.0-dom0.sh && bash /tmp/haveno/0.1-dom0.sh && bash /tmp/haveno/0.2-dom0.sh
```
#### GUI
##### TemplateVM
###### Via `Qubes Manager`:
+ Locate & highlight whonix-workstation-17 (TemplateVM)
+ Right-Click "whonix-workstation-17" and select "Clone qube" from Drop-Down
+ Enter "haveno-template" in "Name"
+ Click OK Button
##### NetVM
###### Via `Qubes Manager`:
+ Click "New qube" Button
+ Enter "sys-haveno" for "Name and label"
+ Click the Button Beside "Name and label" and Select "orange"
+ Select "whonix-gateway-17" from "Template" Drop-Down
+ Select "sys-firewall" from "Networking" Drop-Down
+ Tick "Launch settings after creation" Radio-Box
+ Click OK
+ Click "Advanced" Tab
+ Enter "512" for "Initial memory"
<p style="text-align: center;"><em>(Within reason, can adjust to personal preference)</em></p>
+ Enter "512" for "Max memory"
<p style="text-align: center;"><em>(Within reason, can adjust to personal preference)</em></p>
+ Tick "Provides network" Radio-Box
+ Click "Apply" Button
+ Click "OK" Button
##### AppVM
###### Via `Qubes Manager`:
+ Click "New qube" Button
+ Enter "haveno" for "Name and label"
+ Click the Button Beside "Name and label" and Select "orange"
+ Select "haveno-template" from "Template" Drop-Down
+ Select "sys-haveno" from "Networking" Drop-Down
+ Tick "Launch settings after creation" Radio-Box
+ Click OK
+ Click "Advanced" Tab
+ Enter "2048" for "Initial memory"
<p style="text-align: center;"><em>(Within reason, can adjust to personal preference)</em></p>
+ Enter "4096" for "Max memory"
<p style="text-align: center;"><em>(Within reason, can adjust to personal preference)</em></p>
+ Click "Apply" Button
+ Click "OK" Button
#### CLI
##### TemplateVM
###### In `dom0`:
```shell
$ qvm-clone whonix-workstation-17 haveno-template
```
##### NetVM
##### In `dom0`:
```shell
$ qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True
```
#### AppVM
##### In `dom0`:
```shell
$ qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno
$ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist haveno
```
---
### **Build TemplateVM, NetVM & AppVM:**
#### *TemplateVM Using Precompiled Package via `git` Repository (Scripted)*
##### In `dispXXXX` AppVM:
```shell
% qvm-copy haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh
```
+ Select "haveno-template" for "Target" of Pop-Up
+ Click OK
##### In `haveno-template` TemplateVM:
```shell
% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "<PACKAGE_ARCHIVE_URL>" "<PACKAGE_PGP_FINGERPRINT>"
```
<p style="text-align: center;">Example:</p>
```shell
% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886"
```
#### *TemplateVM Using Precompiled Package From `git` Repository (CLI)*
##### In `haveno-template` TemplateVM:
###### Download & Import Project PGP Key
<p style="text-align: center;">For Whonix On Qubes OS:</p>
```shell
# export https_proxy=http://127.0.0.1:8082
# export KEY_SEARCH="<PACKAGE_PGP_FINGERPRINT>"
# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import
```
<p style="text-align: center;">Example:</p>
```shell
# export https_proxy=http://127.0.0.1:8082
# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886"
# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import
```
<p style="text-align: center;">For Whonix On Anything Other Than Qubes OS:</p>
```shell
# export KEY_SEARCH="<PACKAGE_PGP_FINGERPRINT>"
# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import
```
<p style="text-align: center;">Example:</p>
```shell
# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886"
# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import
```
###### Download Release Files
<p style="text-align: center;">For Whonix On Qubes OS:</p>
```shell
# export https_proxy=http://127.0.0.1:8082
# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt
# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig
# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip
# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig
```
<p style="text-align: center;">Note:</p>
<p style="text-align: center;"><em>Above are dummy URLS which MUST be replaced with actual working URLs</em></p>
<p style="text-align: center;">For Whonix On Anything Other Than Qubes OS:</p>
```shell
# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt
# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig
# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip
# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig
```
<p style="text-align: center;">Note:</p>
<p style="text-align: center;"><em>Above are dummy URLS which MUST be replaced with actual working URLs</em></p>
###### Verify Release Files
```shell
# if gpg --digest-algo SHA256 --verify /tmp/hashes.txt.sig >/dev/null 2>&1; then printf $'SHASUM file has a VALID signature!\n'; else printf $'SHASUMS failed signature check\n' && sleep 5 && exit 1; fi
```
###### Verify Hash, Unpack & Install Package
```shell
# if [[ $(cat /tmp/hashes.txt) =~ $(sha512sum /tmp/haveno*.zip | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && mkdir -p /usr/share/desktop-directories && cd /tmp && unzip /tmp/haveno*.zip && apt install -y /tmp/haveno*.deb; else printf $'WARNING: Bad Hash!\n' && exit; fi
```
###### Verify Jar
```shell
# if [[ $(cat /tmp/desktop*.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi
```
#### *TemplateVM Building From Source via `git` Repository (Scripted)*
##### In `dispXXXX` AppVM:
```shell
% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "<JDK_PACKAGE_URL>" "<JDK_SHA_HASH>" "<SOURCE_URL>"
```
<p style="text-align: center;">Example:</p>
```shell
% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno"
```
+ Upon Successful Compilation & Packaging, A `Filecopy` Confirmation Will Be Presented
+ Select "haveno-template" for "Target" of Pop-Up
+ Click OK
##### In `haveno-template` TemplateVM:
```shell
% sudo apt install -y ./QubesIncoming/dispXXXX/haveno.deb
```
#### *NetVM (Scripted)*
##### In `dispXXXX` AppVM:
```shell
$ qvm-copy haveno/scripts/install_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh
```
+ Select "sys-haveno" for "Target" Within Pop-Up
+ Click "OK" Button
##### In `sys-haveno` NetVM:
(Allow bootstrap process to complete)
```shell
% sudo zsh QubesIncoming/dispXXXX/2.0-haveno-netvm.sh
```
#### *NetVM (CLI)*
##### In `sys-haveno` NetVM:
###### Add `onion-grater` Profile
```shell
# onion-grater-add 40_haveno
```
###### Restart `onion-grater` Service
```shell
# systemctl restart onion-grater.service
# poweroff
```
#### *AppVM (Scripted)*
##### In `dispXXXX` AppVM:
```shell
$ qvm-copy haveno/scripts/install_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh
```
+ Select "haveno" for "Target" of Pop-Up
+ Click OK
##### In `haveno` AppVM:
```shell
% sudo zsh QubesIncoming/dispXXXX/3.0-haveno-appvm.sh
```
#### *AppVM (CLI)*
##### In `haveno` AppVM:
###### Adjust `sdwdate` Configuration
```shell
# mkdir /usr/local/etc/sdwdate-gui.d
# printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf
# systemctl restart sdwdate
```
###### Prepare Firewall Settings via `/rw/config/rc.local`
```shell
# printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local
# printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local
# printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local
```
###### View & Verify Change
```shell
# tail /rw/config/rc.local
```
<p style="text-align: center;"><b>Confirm output contains:</b></p>
> # Poke FW
> printf "EXTERNAL_OPEN_PORTS+=\" 9999 \"\n" | tee /usr/local/etc/whonix_firewall.d/50_user.conf
>
> # Restart FW
> whonix_firewall
###### Restart `whonix_firewall`
```shell
# whonix_firewall
```
###### Create `haveno-Haveno.desktop`
```shell
# mkdir -p /home/$(ls /home)/\.local/share/applications
# sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop
# chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications
```
###### View & Verify Change
```shell
# tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop
```
<p style="text-align: center;"><b>Confirm output contains:</b></p>
> [Desktop Entry]
> Name=Haveno
> Comment=Haveno
> Exec=/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on
> Icon=/opt/haveno/lib/Haveno.png
> Terminal=false
> Type=Application
> Categories=Network
> MimeType=
###### Poweroff
```shell
# poweroff
```
### **Remove TemplateVM, NetVM & AppVM:**
#### Scripted
##### In `dom0`:
```shell
$ bash /tmp/haveno/0.3-dom0.sh
```
#### GUI
##### Via `Qubes Manager`:
+ Highlight "haveno" (AppVM)
+ Click "Delete qube"
+ Enter "haveno"
+ Click "OK" Button
+ Highlight "haveno-template" (TemplateVM)
+ Click "Delete qube"
+ Enter "haveno-template"
+ Click "OK" Button
+ Highlight "sys-haveno" (NetVM)
+ Click "Delete qube"
+ Enter "sys-haveno"
+ Click "OK" Button
#### CLI
##### In `dom0`:
```shell
$ qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno
```

View File

@ -0,0 +1,75 @@
# Install Haveno on Qubes/Whonix
After you already have [`Qubes`](https://www.qubes-os.org/downloads) or [`Whonix`](https://www.whonix.org/wiki/Download) installed:
1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/scripts/install_whonix_qubes/scripts).
2. Move script(s) to their respective destination (`0.*-dom0.sh` -> `dom0`, `1.0-haveno-templatevm.sh` -> `haveno-template`, etc.).
3. Consecutively execute the following commands in their respective destinations.
---
## **Create VMs**
[`Qubes`](https://www.qubes-os.org/downloads)
### **In `dom0`:**
```shell
$ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh
```
[`Whonix`](https://www.whonix.org/wiki/Download) On Anything Other Than [`Qubes`](https://www.qubes-os.org/downloads)
- Clone `Whonix Workstation` To VM Named `haveno-template`
- Clone `Whonix Gateway` To VM Named `sys-haveno`
- Create New Linked VM Clone Based On `haveno-template` Named `haveno`
## **Build TemplateVM**
### *Via Binary Archive*
#### **In `haveno-template` `TemplateVM`:**
```shell
% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "<PACKAGE_ARCHIVE_URL>" "<PACKAGE_PGP_FINGERPRINT>"
```
<p style="text-align: center;">Example:</p>
```shell
% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886"
```
### *Via Source*
#### **In `dispXXXX` `AppVM`:**
```shell
% bash 1.0-haveno-templatevm.sh "<JDK_PACKAGE_URL>" "<JDK_SHA_HASH>" "<SOURCE_URL>"
```
<p style="text-align: center;">Example:</p>
```shell
% bash 1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno"
```
#### **In `haveno-template` `TemplateVM`:**
```shell
% sudo apt install -y haveno.deb
```
## **Build NetVM**
### **In `sys-haveno` `NetVM`:**
```shell
% sudo zsh 3.0-haveno-appvm.sh
```
## **Build AppVM**
### **In `haveno` `AppVM`:**
```shell
% sudo zsh 3.0-haveno-appvm.sh
```
---
Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/scripts/install_whonix_qubes/INSTALL.md).

View File

@ -0,0 +1,6 @@
#!/bin/bash
## ./haveno-on-qubes/scripts/0.0-dom0.sh
## Create Haveno TemplateVM:
qvm-clone whonix-workstation-17 haveno-template

View File

@ -0,0 +1,6 @@
#!/bin/bash
## ./haveno-on-qubes/scripts/0.1-dom0.sh
## Create Haveno NetVM:
qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True

View File

@ -0,0 +1,7 @@
#!/bin/bash
## ./haveno-on-qubes/scripts/0.2-dom0.sh
## Create Haveno AppVM:
qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno
printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist - haveno

View File

@ -0,0 +1,6 @@
#!/bin/bash
## ./haveno-on-qubes/scripts/0.3-dom0.sh
## Remove Haveno GuestVMs
qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno

View File

@ -0,0 +1,185 @@
#!/bin/bash
## ./haveno-on-qubes/scripts/1.1-haveno-templatevm_maker.sh
function remote {
if [[ -z $PRECOMPILED_URL || -z $FINGERPRINT ]]; then
printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nBinary URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n"
exit 1
fi
## Update & Upgrade
apt update && apt upgrade -y
## Install wget
apt install -y wget
## Function to print messages in blue:
echo_blue() {
echo -e "\033[1;34m$1\033[0m"
}
# Function to print error messages in red:
echo_red() {
echo -e "\033[0;31m$1\033[0m"
}
## Sweep for old release files
rm *.asc desktop-*-SNAPSHOT-all.jar.SHA-256 haveno*
## Define URL & PGP Fingerprint etc. vars:
user_url=$PRECOMPILED_URL
base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1')
expected_fingerprint=$FINGERPRINT
binary_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url")
package_filename="haveno.deb"
signature_filename="${binary_filename}.sig"
key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc
wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress"
## Debug:
printf "\nUser URL=$user_url\n"
printf "\nBase URL=$base_url\n"
printf "\nFingerprint=$expected_fingerprint\n"
printf "\nBinary Name=$binary_filename\n"
printf "\nPackage Name=$package_filename\n"
printf "\nSig Filename=$signature_filename\n"
printf "\nKey Filename=$key_filename\n"
## Configure for tinyproxy:
export https_proxy=http://127.0.0.1:8082
## Download Haveno binary:
echo_blue "Downloading Haveno from URL provided ..."
wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; }
## Download Haveno signature file:
echo_blue "Downloading Haveno signature ..."
wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; }
## Download the GPG key:
echo_blue "Downloading signing GPG key ..."
wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; }
## Import the GPG key:
echo_blue "Importing the GPG key ..."
gpg --import "${key_filename}" || { echo_red "Failed to import GPG key."; exit 1; }
## Extract imported fingerprints:
imported_fingerprints=$(gpg --with-colons --fingerprint | grep -A 1 'pub' | grep 'fpr' | cut -d: -f10 | tr -d '\n')
## Remove spaces from the expected fingerprint for comparison:
formatted_expected_fingerprint=$(echo "${expected_fingerprint}" | tr -d ' ')
## Check if the expected fingerprint is in the list of imported fingerprints:
if [[ ! "${imported_fingerprints}" =~ "${formatted_expected_fingerprint}" ]]; then
echo_red "The imported GPG key fingerprint does not match the expected fingerprint."
exit 1
fi
## Verify the downloaded binary with the signature:
echo_blue "Verifying the signature of the downloaded file ..."
if gpg --digest-algo SHA256 --verify "${signature_filename}" >/dev/null 2>&1; then
7z x "${binary_filename}" && mv haveno*.deb "${package_filename}";
else echo_red "Verification failed!" && sleep 5
exit 1;
fi
echo_blue "Haveno binaries have been successfully verified."
# Install Haveno:
echo_blue "Installing Haveno ..."
apt install -y ./"${package_filename}" || { echo_red "Failed to install Haveno."; exit 1; }
## Finalize
echo_blue "Haveno TemplateVM installation and configuration complete."
echo_blue "\nHappy Trading\!\n"
printf "%s \n" "Press [ENTER] to complete ..."
read ans
#exit
poweroff
}
function build {
if [[ -z $JAVA_URL || -z $JAVA_SHA1 || -z $SOURCE_URL ]]; then
printf "\nNo arguments provided!\n\nThis script requires three argument to be provided:\n\nURL for Java 21 JDK Debian Package\n\nSHA1 Hash for Java 21 JDK Debian Package\n\nURL for Remote Git Source Repository\n\nPlease review documentation and try again.\n\nExiting now ...\n"
exit 1
fi
# Dependancies
sudo apt install -y make git expect fakeroot binutils
# Java
curl -fsSLo jdk21.deb ${JAVA_URL}
if [[ $(shasum ./jdk21.deb | awk '{ print $1 }') == ${JAVA_SHA1} ]] ; then printf $'SHA Hash IS valid!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi
sudo apt install -y ./jdk21.deb
# Build
git clone --depth=1 $SOURCE_URL
GIT_DIR=$(awk -F'/' '{ print $NF }' <<< "$SOURCE_URL")
cd ${GIT_DIR}
git checkout master
sed -i 's|XMR_STAGENET|XMR_MAINNET|g' desktop/package/package.gradle
./gradlew clean build --refresh-keys --refresh-dependencies
# Package
# Expect
cat <<DONE >> /tmp/haveno_package_deb.exp
set send_slow {1 .1}
proc send {ignore arg} {
sleep 1.1
exp_send -s -- \$arg
}
set timeout -1
spawn ./gradlew packageInstallers --console=plain
match_max 100000
expect -exact ""
send -- "y\r"
expect -exact ""
send -- "y\r"
expect -exact ""
send -- "y\r"
expect -exact "app-image"
send -- \x03
expect eof
DONE
# Package
expect -f /tmp/haveno_package_deb.exp && find ./ -name '*.deb' -exec qvm-copy {} \;
printf "\nHappy Trading!\n"
}
if ! [[ $# -eq 2 || $# -eq 3 ]] ; then
printf "\nFor this script to function, user supplied arguments are required.\n\n"
printf "\nPlease review documentation and try again.\n\n"
fi
if [[ $# -eq 2 ]] ; then
PRECOMPILED_URL=$1
FINGERPRINT=$2
remote
fi
if [[ $# -eq 3 ]] ; then
JAVA_URL=$1
JAVA_SHA1=$2
SOURCE_URL=$3
build
fi

View File

@ -0,0 +1,30 @@
#!/bin/zsh
## ./haveno-on-qubes/scripts/2.0-haveno-netvm_taker.sh
## Function to print messages in blue:
echo_blue() {
echo -e "\033[1;34m$1\033[0m"
}
# Function to print error messages in red:
echo_red() {
echo -e "\033[0;31m$1\033[0m"
}
## onion-grater
# Add onion-grater Profile
echo_blue "\nAdding onion-grater Profile ..."
onion-grater-add 40_haveno
# Restart onion-grater
echo_blue "\nRestarting onion-grater Service ..."
systemctl restart onion-grater.service
echo_blue "Haveno NetVM configuration complete."
printf "%s \n" "Press [ENTER] to complete ..."
read ans
#exit
poweroff

View File

@ -0,0 +1,61 @@
#!/bin/zsh
## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh
## Function to print messages in blue:
echo_blue() {
echo -e "\033[1;34m$1\033[0m"
}
# Function to print error messages in red:
echo_red() {
echo -e "\033[0;31m$1\033[0m"
}
## Adjust sdwdate Configuration
mkdir -p /usr/local/etc/sdwdate-gui.d
printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf
systemctl restart sdwdate
## Prepare Firewall Settings
echo_blue "\nConfiguring FW ..."
printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local
printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local
printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local
## View & Verify Change
echo_blue "\nReview the following output and be certain in matches documentation!\n"
tail /rw/config/rc.local
printf "%s \n" "Press [ENTER] to continue ..."
read ans
:
## Restart FW
echo_blue "\nRestarting Whonix FW ..."
whonix_firewall
### Create Desktop Launcher:
echo_blue "Creating desktop launcher ..."
mkdir -p /home/$(ls /home)/\.local/share/applications
sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --torControlUseSafeCookieAuth --torControlCookieFile=/var/run/tor/control.authcookie --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop
chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop
## View & Verify Change
echo_blue "\nReview the following output and be certain in matches documentation!\n"
tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop
printf "%s \n" "Press [ENTER] to continue ..."
read ans
:
echo_blue "Haveno AppVM configuration complete."
echo_blue "Refresh applications via Qubes Manager GUI now."
printf "%s \n" "Press [ENTER] to complete ..."
read ans
#exit
poweroff

View File

@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final String VERSION = "1.0.18";
private static final String VERSION = "1.0.19";
private SeedNode seedNode;
private Timer checkConnectionLossTime;