refactoring based on congestion testing

retry creating and processing trade txs on failure
do not use connection manager polling to reduce requests
use global daemon lock for wallet sync operations
sync wallets on poll if behind
use local util to get payment uri to avoid blocking
all peers share multisig hex on deposits confirmed
import multisig hex when needed
This commit is contained in:
woodser 2024-04-29 07:02:05 -04:00
parent f519ac12a5
commit e63141279c
36 changed files with 799 additions and 568 deletions

View file

@ -199,15 +199,15 @@ public class CoreApi {
// Monero Connections
///////////////////////////////////////////////////////////////////////////////////////////
public void addMoneroConnection(MoneroRpcConnection connection) {
public void addXmrConnection(MoneroRpcConnection connection) {
xmrConnectionService.addConnection(connection);
}
public void removeMoneroConnection(String connectionUri) {
public void removeXmrConnection(String connectionUri) {
xmrConnectionService.removeConnection(connectionUri);
}
public MoneroRpcConnection getMoneroConnection() {
public MoneroRpcConnection getXmrConnection() {
return xmrConnectionService.getConnection();
}
@ -215,15 +215,15 @@ public class CoreApi {
return xmrConnectionService.getConnections();
}
public void setMoneroConnection(String connectionUri) {
public void setXmrConnection(String connectionUri) {
xmrConnectionService.setConnection(connectionUri);
}
public void setMoneroConnection(MoneroRpcConnection connection) {
public void setXmrConnection(MoneroRpcConnection connection) {
xmrConnectionService.setConnection(connection);
}
public MoneroRpcConnection checkMoneroConnection() {
public MoneroRpcConnection checkXmrConnection() {
return xmrConnectionService.checkConnection();
}
@ -231,19 +231,19 @@ public class CoreApi {
return xmrConnectionService.checkConnections();
}
public void startCheckingMoneroConnection(Long refreshPeriod) {
public void startCheckingXmrConnection(Long refreshPeriod) {
xmrConnectionService.startCheckingConnection(refreshPeriod);
}
public void stopCheckingMoneroConnection() {
public void stopCheckingXmrConnection() {
xmrConnectionService.stopCheckingConnection();
}
public MoneroRpcConnection getBestAvailableMoneroConnection() {
public MoneroRpcConnection getBestAvailableXmrConnection() {
return xmrConnectionService.getBestAvailableConnection();
}
public void setMoneroConnectionAutoSwitch(boolean autoSwitch) {
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
xmrConnectionService.setAutoSwitch(autoSwitch);
}

View file

@ -90,6 +90,7 @@ public final class XmrConnectionService {
private boolean isInitialized;
private boolean pollInProgress;
private MoneroDaemonRpc daemon;
private Boolean isConnected = false;
@Getter
private MoneroDaemonInfo lastInfo;
private Long syncStartHeight = null;
@ -148,7 +149,6 @@ public final class XmrConnectionService {
isInitialized = false;
synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop();
connectionManager.stopPolling();
daemon = null;
}
}
@ -171,7 +171,7 @@ public final class XmrConnectionService {
}
public Boolean isConnected() {
return connectionManager.isConnected();
return isConnected;
}
public void addConnection(MoneroRpcConnection connection) {
@ -196,6 +196,12 @@ public final class XmrConnectionService {
return connectionManager.getConnections();
}
public void switchToBestConnection() {
if (isFixedConnection() || !connectionManager.getAutoSwitch()) return;
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection);
}
public void setConnection(String connectionUri) {
accountService.checkAccountOpen();
connectionManager.setConnection(connectionUri); // listener will update connection list
@ -226,8 +232,8 @@ public final class XmrConnectionService {
public void stopCheckingConnection() {
accountService.checkAccountOpen();
connectionManager.stopPolling();
connectionList.setRefreshPeriod(-1L);
updatePolling();
}
public MoneroRpcConnection getBestAvailableConnection() {
@ -472,8 +478,6 @@ public final class XmrConnectionService {
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection);
} else {
checkConnection();
}
} else if (!isInitialized) {
@ -485,19 +489,11 @@ public final class XmrConnectionService {
// start local node if applicable
maybeStartLocalNode();
// update connection
checkConnection();
}
// register connection listener
connectionManager.addListener(this::onConnectionChanged);
// start polling after delay
UserThread.runAfter(() -> {
if (!isShutDownStarted) connectionManager.startPolling(getRefreshPeriodMs() * 2);
}, getDefaultRefreshPeriodMs() * 2 / 1000);
isInitialized = true;
}
@ -524,7 +520,6 @@ public final class XmrConnectionService {
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted) return;
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected());
if (currentConnection == null) {
log.warn("Setting daemon connection to null");
Thread.dumpStack();
@ -532,9 +527,11 @@ public final class XmrConnectionService {
synchronized (lock) {
if (currentConnection == null) {
daemon = null;
isConnected = false;
connectionList.setCurrentConnectionUri(null);
} else {
daemon = new MoneroDaemonRpc(currentConnection);
isConnected = currentConnection.isConnected();
connectionList.removeConnection(currentConnection.getUri());
connectionList.addConnection(currentConnection);
connectionList.setCurrentConnectionUri(currentConnection.getUri());
@ -546,9 +543,13 @@ public final class XmrConnectionService {
numUpdates.set(numUpdates.get() + 1);
});
}
updatePolling();
// update polling
doPollDaemon();
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000);
// notify listeners in parallel
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
synchronized (listenerLock) {
for (MoneroConnectionManagerListener listener : listeners) {
ThreadUtils.submitToPool(() -> listener.onConnectionChanged(currentConnection));
@ -557,18 +558,14 @@ public final class XmrConnectionService {
}
private void updatePolling() {
new Thread(() -> {
synchronized (lock) {
stopPolling();
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
}
}).start();
stopPolling();
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
}
private void startPolling() {
synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop();
daemonPollLooper = new TaskLooper(() -> pollDaemonInfo());
daemonPollLooper = new TaskLooper(() -> pollDaemon());
daemonPollLooper.start(getRefreshPeriodMs());
}
}
@ -582,17 +579,34 @@ public final class XmrConnectionService {
}
}
private void pollDaemonInfo() {
private void pollDaemon() {
if (pollInProgress) return;
doPollDaemon();
}
private void doPollDaemon() {
synchronized (pollLock) {
pollInProgress = true;
if (isShutDownStarted) return;
try {
// poll daemon
log.debug("Polling daemon info");
if (daemon == null) throw new RuntimeException("No daemon connection");
lastInfo = daemon.getInfo();
if (daemon == null) switchToBestConnection();
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
try {
lastInfo = daemon.getInfo();
} catch (Exception e) {
try {
log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage());
switchToBestConnection();
lastInfo = daemon.getInfo();
} catch (Exception e2) {
throw e2; // caught internally
}
}
// connected to daemon
isConnected = true;
// update properties on user thread
UserThread.execute(() -> {
@ -632,19 +646,15 @@ public final class XmrConnectionService {
lastErrorTimestamp = null;
}
// update and notify connected state
if (!Boolean.TRUE.equals(connectionManager.isConnected())) {
connectionManager.checkConnection();
}
// clear error message
if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
}
if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
} catch (Exception e) {
// skip if shut down or connected
if (isShutDownStarted || Boolean.TRUE.equals(isConnected())) return;
// not connected to daemon
isConnected = false;
// skip if shut down
if (isShutDownStarted) return;
// log error message periodically
if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) {
@ -653,20 +663,8 @@ public final class XmrConnectionService {
if (DevEnv.isDevMode()) e.printStackTrace();
}
new Thread(() -> {
if (isShutDownStarted) return;
if (!isFixedConnection() && connectionManager.getAutoSwitch()) {
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) connectionManager.setConnection(bestConnection);
} else {
connectionManager.checkConnection();
}
// set error message
if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
}
}).start();
// set error message
if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
} finally {
pollInProgress = false;
}

View file

@ -36,7 +36,6 @@ package haveno.core.offer;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import common.utils.GenUtils;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.file.JsonFileManager;
@ -46,6 +45,7 @@ import haveno.core.api.XmrConnectionService;
import haveno.core.filter.FilterManager;
import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller;
@ -287,7 +287,7 @@ public class OfferBookService {
// first poll after 20s
// TODO: remove?
new Thread(() -> {
GenUtils.waitFor(20000);
HavenoUtils.waitFor(20000);
keyImagePoller.poll();
}).start();
}

View file

@ -36,7 +36,6 @@ package haveno.core.offer;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.inject.Inject;
import common.utils.GenUtils;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
@ -71,6 +70,7 @@ import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.TradableList;
import haveno.core.trade.handlers.TransactionResultHandler;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences;
import haveno.core.user.User;
@ -278,7 +278,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// first poll in 5s
// TODO: remove?
new Thread(() -> {
GenUtils.waitFor(5000);
HavenoUtils.waitFor(5000);
signedOfferKeyImagePoller.poll();
}).start();
}
@ -349,7 +349,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// For typical number of offers we are tolerant with delay to give enough time to broadcast.
// If number of offers is very high we limit to 3 sec. to not delay other shutdown routines.
long delayMs = Math.min(3000, size * 200 + 500);
GenUtils.waitFor(delayMs);
HavenoUtils.waitFor(delayMs);
}, THREAD_ID);
} else {
broadcaster.flush();
@ -705,9 +705,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// remove open offer which thaws its key images
private void onCancelled(@NotNull OpenOffer openOffer) {
Offer offer = openOffer.getOffer();
if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
}
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
offer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED);
removeOpenOffer(openOffer);
@ -1029,6 +1027,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
if (sufficientAvailableBalance) {
log.info("Splitting and scheduling outputs for offer {} at subaddress {}", openOffer.getShortId());
splitAndSchedule(openOffer);
} else if (openOffer.getScheduledTxHashes() == null) {
scheduleWithEarliestTxs(openOffers, openOffer);
@ -1038,16 +1037,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) {
BigInteger reserveAmount = openOffer.getOffer().getReserveAmount();
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getId(), entry.getSubaddressIndex());
long startTime = System.currentTimeMillis();
MoneroTxWallet splitOutputTx = xmrWalletService.getWallet().createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setAddress(entry.getAddressString())
.setAmount(reserveAmount)
.setRelay(true)
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime);
MoneroTxWallet splitOutputTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
long startTime = System.currentTimeMillis();
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setAddress(entry.getAddressString())
.setAmount(reserveAmount)
.setRelay(true)
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
break;
} catch (Exception e) {
log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime);
}
// schedule txs
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());

View file

@ -133,7 +133,7 @@ public class PlaceOfferProtocol {
stopTimeoutTimer();
timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing"));
}, TradeProtocol.TRADE_TIMEOUT_SECONDS);
}, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
}
private void stopTimeoutTimer() {

View file

@ -17,12 +17,22 @@
package haveno.core.offer.placeoffer.tasks;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
@ -35,7 +45,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
@Override
protected void run() {
Offer offer = model.getOpenOffer().getOffer();
OpenOffer openOffer = model.getOpenOffer();
Offer offer = openOffer.getOffer();
try {
runInterceptHook();
@ -44,16 +55,51 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getXmrWalletService().getConnectionService().verifyConnection();
// create reserve tx
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(model.getOpenOffer());
model.setReserveTx(reserveTx);
MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
// check for error in case creating reserve tx exceeded timeout // TODO: better way?
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) {
throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted");
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct());
BigInteger makerFee = offer.getMaxMakerFee();
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// attempt creating reserve tx
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, offerId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for error in case creating reserve tx exceeded timeout // TODO: better way?
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) {
throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted");
}
if (reserveTx != null) break;
}
}
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// update offer state
openOffer.setReserveTxHash(reserveTx.getHash());
openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
}
// reset protocol timeout
model.getProtocol().startTimeoutTimer();
model.setReserveTx(reserveTx);
complete();
} catch (Throwable t) {
offer.setErrorMessage("An error occurred.\n" +

View file

@ -524,7 +524,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
if (trade.walletExists()) trade.importMultisigHex();
// add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
@ -896,17 +895,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
// create dispute payout tx
MoneroTxWallet payoutTx = null;
try {
payoutTx = trade.getWallet().createTx(txConfig);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Loser payout is too small to cover the mining fee");
}
MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig);
// update trade state
if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());

View file

@ -36,7 +36,6 @@ package haveno.core.support.dispute.arbitration;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import common.utils.GenUtils;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
@ -68,6 +67,7 @@ import haveno.core.trade.Contract;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.wallet.TradeWalletService;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.AckMessageSourceType;
@ -290,7 +290,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
for (int i = 0; i < 5; i++) {
if (trade.isPayoutPublished()) break;
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
}
if (!trade.isPayoutPublished()) trade.syncAndPollWallet();
}
@ -310,7 +310,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} else {
if (e instanceof IllegalArgumentException) throw e;
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId, e);
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
}
}
}
@ -430,8 +430,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (!expectedBuyerAmount.equals(actualBuyerAmount)) throw new IllegalArgumentException("Unexpected buyer payout: " + expectedBuyerAmount + " vs " + actualBuyerAmount);
if (!expectedSellerAmount.equals(actualSellerAmount)) throw new IllegalArgumentException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount);
// check wallet's daemon connection
trade.checkAndVerifyDaemonConnection();
// check daemon connection
trade.verifyDaemonConnection();
// determine if we already signed dispute payout tx
// TODO: better way, such as by saving signed dispute payout tx hex in designated field instead of shared payoutTxHex field?
@ -471,8 +471,17 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
// submit fully signed payout tx to the network
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
break;
} catch (Exception e) {
log.warn("Failed to submit dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
// update state
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?

View file

@ -19,6 +19,8 @@ package haveno.core.trade;
import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets;
import common.utils.GenUtils;
import haveno.common.config.Config;
import haveno.common.crypto.CryptoException;
import haveno.common.crypto.Hash;
@ -70,6 +72,17 @@ public class HavenoUtils {
public static final double TAKER_FEE_PCT = 0.0075; // 0.75%
public static final double PENALTY_FEE_PCT = 0.02; // 2%
// synchronize requests to the daemon
private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool)
private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create tx, import multisig hex)
private static Object DAEMON_LOCK = new Object();
public static Object getDaemonLock() {
return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object();
}
public static Object getWalletFunctionLock() {
return SYNC_WALLET_REQUESTS ? getDaemonLock() : new Object();
}
// non-configurable
public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // use the US locale as a base for all DecimalFormats (commas should be omitted from number strings)
public static int XMR_SMALLEST_UNIT_EXPONENT = 12;
@ -108,6 +121,10 @@ public class HavenoUtils {
return new Date().before(releaseDatePlusDays);
}
public static void waitFor(long waitMs) {
GenUtils.waitFor(waitMs);
}
// ----------------------- CONVERSION UTILS -------------------------------
public static BigInteger coinToAtomicUnits(Coin coin) {

View file

@ -37,7 +37,6 @@ package haveno.core.trade;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import common.utils.GenUtils;
import haveno.common.ThreadUtils;
import haveno.common.UserThread;
import haveno.common.crypto.Encryption;
@ -120,7 +119,6 @@ import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@ -479,7 +477,6 @@ public abstract class Trade implements Tradable, Model {
private long payoutTxFee;
private Long payoutHeight;
private IdlePayoutSyncer idlePayoutSyncer;
@Getter
@Setter
private boolean isCompleted;
@ -638,18 +635,14 @@ public abstract class Trade implements Tradable, Model {
// handle trade state events
tradeStateSubscription = EasyBind.subscribe(stateProperty, newValue -> {
if (!isInitialized || isShutDownStarted) return;
ThreadUtils.execute(() -> {
if (newValue == Trade.State.MULTISIG_COMPLETED) {
updatePollPeriod();
startPolling();
}
}, getId());
// no processing
});
// handle trade phase events
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
if (!isInitialized || isShutDownStarted) return;
ThreadUtils.execute(() -> {
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
if (isPaymentReceived()) {
UserThread.execute(() -> {
@ -674,9 +667,9 @@ public abstract class Trade implements Tradable, Model {
// sync main wallet to update pending balance
new Thread(() -> {
GenUtils.waitFor(1000);
HavenoUtils.waitFor(1000);
if (isShutDownStarted) return;
if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) xmrWalletService.syncWallet(xmrWalletService.getWallet());
if (xmrConnectionService.isConnected()) xmrWalletService.syncWallet();
}).start();
// complete disputed trade
@ -731,16 +724,17 @@ public abstract class Trade implements Tradable, Model {
setPayoutStateUnlocked();
return;
} else {
throw new IllegalStateException("Missing trade wallet for " + getClass().getSimpleName() + " " + getId());
log.warn("Missing trade wallet for {} {}, state={}, marked completed={}", getClass().getSimpleName(), getShortId(), getState(), isCompleted());
return;
}
}
// initialize syncing and polling
tryInitPolling();
// start polling if deposit requested
if (isDepositRequested()) tryInitPolling();
}
public void requestPersistence() {
processModel.getTradeManager().requestPersistence();
if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence();
}
public TradeProtocol getProtocol() {
@ -793,21 +787,8 @@ public abstract class Trade implements Tradable, Model {
return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid();
}
public void checkAndVerifyDaemonConnection() {
// check connection which might update
xmrConnectionService.checkConnection();
xmrConnectionService.verifyConnection();
// check wallet connection on same thread as connection change
CountDownLatch latch = new CountDownLatch(1);
ThreadUtils.submitToPool((() -> {
ThreadUtils.execute(() -> {
if (!isWalletConnectedToDaemon()) throw new RuntimeException("Trade wallet is not connected to a Monero node"); // wallet connection is updated on trade thread
latch.countDown();
}, getConnectionChangedThreadId());
}));
HavenoUtils.awaitLatch(latch); // TODO: better way?
public void verifyDaemonConnection() {
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Connection service is not connected to a Monero node");
}
public boolean isWalletConnectedToDaemon() {
@ -848,7 +829,7 @@ public abstract class Trade implements Tradable, Model {
// reset wallet poll period after duration
new Thread(() -> {
GenUtils.waitFor(pollNormalDuration);
HavenoUtils.waitFor(pollNormalDuration);
Long pollNormalStartTimeMsCopy = pollNormalStartTimeMs; // copy to avoid race condition
if (pollNormalStartTimeMsCopy == null) return;
if (!isShutDown && System.currentTimeMillis() >= pollNormalStartTimeMsCopy + pollNormalDuration) {
@ -860,21 +841,38 @@ public abstract class Trade implements Tradable, Model {
public void importMultisigHex() {
synchronized (walletLock) {
// ensure wallet sees deposits confirmed
if (!isDepositsConfirmed()) syncAndPollWallet();
// import multisig hexes
List<String> multisigHexes = new ArrayList<String>();
for (TradePeer node : getAllTradeParties()) if (node.getUpdatedMultisigHex() != null) multisigHexes.add(node.getUpdatedMultisigHex());
if (!multisigHexes.isEmpty()) {
log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId());
long startTime = System.currentTimeMillis();
getWallet().importMultisigHex(multisigHexes.toArray(new String[0]));
log.info("Done importing multisig hex for {} {} in {} ms", getClass().getSimpleName(), getId(), System.currentTimeMillis() - startTime);
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
doImportMultisigHex();
break;
} catch (Exception e) {
log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
}
}
}
private void doImportMultisigHex() {
// ensure wallet sees deposits confirmed
if (!isDepositsConfirmed()) syncAndPollWallet();
// collect multisig hex from peers
List<String> multisigHexes = new ArrayList<String>();
for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex());
// import multisig hex
log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size());
long startTime = System.currentTimeMillis();
if (!multisigHexes.isEmpty()) {
wallet.importMultisigHex(multisigHexes.toArray(new String[0]));
requestSaveWallet();
}
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
}
public void changeWalletPassword(String oldPassword, String newPassword) {
@ -891,10 +889,10 @@ public abstract class Trade implements Tradable, Model {
public void saveWallet() {
synchronized (walletLock) {
if (!walletExists()) {
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId());
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getShortId());
return;
}
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId());
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getShortId());
xmrWalletService.saveWallet(wallet);
maybeBackupWallet();
}
@ -953,7 +951,13 @@ public abstract class Trade implements Tradable, Model {
// check for balance
if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance");
synchronized (HavenoUtils.getDaemonLock()) {
log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId());
wallet.rescanSpent();
if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance of " + wallet.getBalance());
}
}
}
// force close wallet without warning
@ -1021,17 +1025,44 @@ public abstract class Trade implements Tradable, Model {
return contract;
}
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
return wallet.createTx(txConfig);
}
}
}
/**
* Create the payout tx.
*
* @return MoneroTxWallet the payout tx when the trade is successfully completed
* @return the payout tx when the trade is successfully completed
*/
public MoneroTxWallet createPayoutTx() {
// check connection to monero daemon
checkAndVerifyDaemonConnection();
verifyDaemonConnection();
// check multisig import
// create payout tx
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
return doCreatePayoutTx();
} catch (Exception e) {
log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId());
}
}
}
private MoneroTxWallet doCreatePayoutTx() {
// check if multisig import needed
MoneroWallet multisigWallet = getWallet();
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
@ -1047,7 +1078,7 @@ public abstract class Trade implements Tradable, Model {
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
// create payout tx
MoneroTxWallet payoutTx = multisigWallet.createTx(new MoneroTxConfig()
MoneroTxWallet payoutTx = createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount)
.addDestination(sellerPayoutAddress, sellerPayoutAmount)
@ -1066,6 +1097,24 @@ public abstract class Trade implements Tradable, Model {
return payoutTx;
}
public MoneroTxWallet createDisputePayoutTx(MoneroTxConfig txConfig) {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
return createTx(txConfig);
} catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee");
log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId());
}
}
}
/**
* Process a payout tx.
*
@ -1074,82 +1123,93 @@ public abstract class Trade implements Tradable, Model {
* @param publish publishes the signed payout tx if true
*/
public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId());
synchronized (walletLock) {
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId());
// gather relevant info
MoneroWallet wallet = getWallet();
Contract contract = getContract();
BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = getAmount();
// describe payout tx
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);
// 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");
// get buyer and seller destinations (order not preserved)
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1);
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
// verify payout addresses
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract");
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract");
// verify change address is multisig's primary address
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount());
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount");
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2));
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit);
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
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);
// check wallet connection
if (sign || publish) checkAndVerifyDaemonConnection();
// handle tx signing
if (sign) {
// sign tx
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
payoutTxHex = result.getSignedMultisigTxHex();
// describe result
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
payoutTx = describedTxSet.getTxs().get(0);
// 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?
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);
}
// update trade state
setPayoutTx(payoutTx);
setPayoutTxHex(payoutTxHex);
// submit payout tx
if (publish) {
wallet.submitMultisigTxHex(payoutTxHex);
pollWallet();
// gather relevant info
MoneroWallet wallet = getWallet();
Contract contract = getContract();
BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = getAmount();
// describe payout tx
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);
// 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");
// get buyer and seller destinations (order not preserved)
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1);
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
// verify payout addresses
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract");
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract");
// verify change address is multisig's primary address
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount());
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount");
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2));
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit);
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
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);
// check connection
if (sign || publish) verifyDaemonConnection();
// handle tx signing
if (sign) {
// sign tx
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
payoutTxHex = result.getSignedMultisigTxHex();
// describe result
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
payoutTx = describedTxSet.getTxs().get(0);
// 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?
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);
}
// update trade state
setPayoutTx(payoutTx);
setPayoutTxHex(payoutTxHex);
// submit payout tx
if (publish) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
wallet.submitMultisigTxHex(payoutTxHex);
break;
} catch (Exception e) {
log.warn("Failed to submit payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
}
}
pollWallet();
}
/**
@ -1173,6 +1233,7 @@ public abstract class Trade implements Tradable, Model {
// set payment account payload
getTradePeer().setPaymentAccountPayload(paymentAccountPayload);
processModel.getPaymentAccountDecryptedProperty().set(true);
} catch (Exception e) {
throw new RuntimeException(e);
}
@ -1220,6 +1281,7 @@ public abstract class Trade implements Tradable, Model {
public void clearAndShutDown() {
ThreadUtils.execute(() -> {
clearProcessData();
onShutDownStarted();
ThreadUtils.submitToPool(() -> shutDown()); // run off trade thread
}, getId());
}
@ -1237,7 +1299,7 @@ public abstract class Trade implements Tradable, Model {
// TODO: clear other process data
setPayoutTxHex(null);
for (TradePeer peer : getAllTradeParties()) {
for (TradePeer peer : getAllPeers()) {
peer.setUnsignedPayoutTxHex(null);
peer.setUpdatedMultisigHex(null);
peer.setDisputeClosedMessage(null);
@ -1294,7 +1356,7 @@ public abstract class Trade implements Tradable, Model {
// repeatedly acquire lock to clear tasks
for (int i = 0; i < 20; i++) {
synchronized (this) {
GenUtils.waitFor(10);
HavenoUtils.waitFor(10);
}
}
@ -1390,6 +1452,7 @@ public abstract class Trade implements Tradable, Model {
}
this.state = state;
requestPersistence();
UserThread.await(() -> {
stateProperty.set(state);
phaseProperty.set(state.getPhase());
@ -1421,6 +1484,7 @@ public abstract class Trade implements Tradable, Model {
}
this.payoutState = payoutState;
requestPersistence();
UserThread.await(() -> payoutStateProperty.set(payoutState));
}
@ -1572,13 +1636,13 @@ public abstract class Trade implements Tradable, Model {
throw new RuntimeException("Trade is not maker, taker, or arbitrator");
}
private List<TradePeer> getPeers() {
List<TradePeer> peers = getAllTradeParties();
private List<TradePeer> getOtherPeers() {
List<TradePeer> peers = getAllPeers();
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers;
}
private List<TradePeer> getAllTradeParties() {
private List<TradePeer> getAllPeers() {
List<TradePeer> peers = new ArrayList<TradePeer>();
peers.add(getMaker());
peers.add(getTaker());
@ -1765,7 +1829,7 @@ public abstract class Trade implements Tradable, Model {
if (this instanceof BuyerTrade) {
return getArbitrator().isDepositsConfirmedMessageAcked();
} else {
for (TradePeer peer : getPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false;
for (TradePeer peer : getOtherPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false;
return true;
}
}
@ -1982,13 +2046,19 @@ public abstract class Trade implements Tradable, Model {
}
// sync and reprocess messages on new thread
if (isInitialized && connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) {
ThreadUtils.execute(() -> tryInitPolling(), getId());
}
}
}
private void tryInitPolling() {
if (isShutDownStarted) return;
// set known deposit txs
List<MoneroTxWallet> depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false));
setDepositTxs(depositTxs);
// start polling
if (!isIdling()) {
tryInitPollingAux();
} else {
@ -2023,9 +2093,12 @@ public abstract class Trade implements Tradable, Model {
private void syncWallet(boolean pollWallet) {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
xmrWalletService.syncWallet(getWallet());
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
if (isWalletBehind()) {
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId());
long startTime = System.currentTimeMillis();
syncWalletIfBehind();
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
}
// apply tor after wallet synced depending on configuration
if (!wasWalletSynced) {
@ -2063,6 +2136,7 @@ public abstract class Trade implements Tradable, Model {
private void startPolling() {
synchronized (walletLock) {
if (isShutDownStarted || isPollInProgress()) return;
updatePollPeriod();
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
pollLooper = new TaskLooper(() -> pollWallet());
pollLooper.start(pollPeriodMs);
@ -2110,7 +2184,15 @@ public abstract class Trade implements Tradable, Model {
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
List<MoneroTxWallet> txs = wallet.getTxs(query);
List<MoneroTxWallet> txs;
if (!updatePool) txs = wallet.getTxs(query);
else {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) {
txs = wallet.getTxs(query);
}
}
}
setDepositTxs(txs);
if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen
setStateDepositsSeen();
@ -2142,7 +2224,7 @@ public abstract class Trade implements Tradable, Model {
if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind();
// rescan spent outputs to detect unconfirmed payout tx
if (isPayoutExpected && !isPayoutPublished()) {
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
try {
wallet.rescanSpent();
} catch (Exception e) {
@ -2154,7 +2236,15 @@ public abstract class Trade implements Tradable, Model {
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
boolean updatePool = isPayoutExpected && !isPayoutConfirmed();
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
List<MoneroTxWallet> txs = wallet.getTxs(query);
List<MoneroTxWallet> txs = null;
if (!updatePool) txs = wallet.getTxs(query);
else {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) {
txs = wallet.getTxs(query);
}
}
}
setDepositTxs(txs);
// check if any outputs spent (observed on payout published)
@ -2191,7 +2281,15 @@ public abstract class Trade implements Tradable, Model {
}
private void syncWalletIfBehind() {
if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) syncWallet(false);
if (isWalletBehind()) {
synchronized (walletLock) {
xmrWalletService.syncWallet(wallet);
}
}
}
private boolean isWalletBehind() {
return wallet.getHeight() < xmrConnectionService.getTargetHeight();
}
private void setDepositTxs(List<? extends MoneroTx> txs) {
@ -2278,9 +2376,8 @@ public abstract class Trade implements Tradable, Model {
processing = false;
} catch (Exception e) {
processing = false;
boolean isWalletConnected = isWalletConnectedToDaemon();
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected
if (isInitialized &&!isShutDownStarted && isWalletConnected) {
if (!isInitialized || isShutDownStarted) return;
if (isWalletConnectedToDaemon()) {
e.printStackTrace();
log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
};

View file

@ -38,7 +38,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import common.utils.GenUtils;
import haveno.common.ClockWatcher;
import haveno.common.ThreadUtils;
import haveno.common.UserThread;
@ -512,7 +511,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}).start();
// allow execution to start
GenUtils.waitFor(100);
HavenoUtils.waitFor(100);
}
private void initPersistedTrade(Trade trade) {
@ -1249,27 +1248,20 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
private void addTrade(Trade trade) {
UserThread.execute(() -> {
synchronized (tradableList) {
if (tradableList.add(trade)) {
requestPersistence();
}
synchronized (tradableList) {
if (tradableList.add(trade)) {
requestPersistence();
}
});
}
}
private void removeTrade(Trade trade) {
log.info("TradeManager.removeTrade() " + trade.getId());
synchronized (tradableList) {
if (!tradableList.contains(trade)) return;
}
// remove trade
UserThread.execute(() -> {
synchronized (tradableList) {
tradableList.remove(trade);
}
});
synchronized (tradableList) {
if (!tradableList.remove(trade)) return;
}
// unregister and persist
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
@ -1277,30 +1269,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
private void maybeRemoveTradeOnError(Trade trade) {
synchronized (tradableList) {
if (trade.isDepositRequested() && !trade.isDepositFailed()) {
listenForCleanup(trade);
} else {
removeTradeOnError(trade);
}
if (trade.isDepositRequested() && !trade.isDepositFailed()) {
listenForCleanup(trade);
} else {
removeTradeOnError(trade);
}
}
private void removeTradeOnError(Trade trade) {
log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState());
synchronized (tradableList) {
// unreserve taker key images
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages());
trade.getSelf().setReserveTxKeyImages(null);
}
// unreserve taker key images
if (trade instanceof TakerTrade) {
xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages());
trade.getSelf().setReserveTxKeyImages(null);
}
// unreserve open offer
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(trade.getId());
if (trade instanceof MakerTrade && openOffer.isPresent()) {
openOfferManager.unreserveOpenOffer(openOffer.get());
}
// unreserve open offer
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(trade.getId());
if (trade instanceof MakerTrade && openOffer.isPresent()) {
openOfferManager.unreserveOpenOffer(openOffer.get());
}
// clear and shut down trade
@ -1358,7 +1346,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
new Thread(() -> {
// wait minimum time
GenUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime)));
HavenoUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime)));
// get trade's deposit txs from daemon
MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());

View file

@ -59,13 +59,13 @@ public class ArbitratorProtocol extends DisputeProtocol {
ArbitratorSendInitTradeOrMultisigRequests.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}
@ -100,7 +100,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
errorMessage -> {
handleTaskRunnerFault(sender, request, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}

View file

@ -74,13 +74,13 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
MakerSendInitTradeRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}

View file

@ -79,13 +79,13 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
TakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
unlatchTrade();
},
errorMessage -> {
handleError(errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}

View file

@ -46,6 +46,7 @@ import haveno.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator;
import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller;
import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller;
import haveno.core.trade.protocol.tasks.TradeTask;
import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j;
@ -158,6 +159,6 @@ public class BuyerProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked")
@Override
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
return new Class[] { SendDepositsConfirmedMessageToSeller.class, SendDepositsConfirmedMessageToArbitrator.class };
}
}

View file

@ -163,6 +163,7 @@ public class ProcessModel implements Model, PersistablePayload {
private ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@Setter
private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private ObjectProperty<Boolean> paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false);
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer());

View file

@ -79,13 +79,13 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
MakerSendInitTradeRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}

View file

@ -80,13 +80,13 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
TakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
unlatchTrade();
},
errorMessage -> {
handleError(errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}

View file

@ -37,6 +37,7 @@ package haveno.core.trade.protocol;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.proto.network.NetworkEnvelope;
@ -67,7 +68,7 @@ import haveno.core.trade.protocol.tasks.ProcessInitMultisigRequest;
import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage;
import haveno.core.trade.protocol.tasks.ProcessSignContractRequest;
import haveno.core.trade.protocol.tasks.ProcessSignContractResponse;
import haveno.core.trade.protocol.tasks.SendDepositRequest;
import haveno.core.trade.protocol.tasks.RemoveOffer;
import haveno.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout;
@ -93,8 +94,10 @@ import java.util.concurrent.CountDownLatch;
@Slf4j
public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener {
public static final int TRADE_TIMEOUT_SECONDS = 120;
public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 45 : 180;
private static final String TIMEOUT_REACHED = "Timeout reached.";
public static final int MAX_ATTEMPTS = 3;
public static final long REPROCESS_DELAY_MS = 5000;
protected final ProcessModel processModel;
protected final Trade trade;
@ -104,6 +107,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected TradeResultHandler tradeResultHandler;
protected ErrorMessageHandler errorMessageHandler;
private boolean depositsConfirmedTasksCalled;
private int reprocessPaymentReceivedMessageCount;
///////////////////////////////////////////////////////////////////////////////////////////
@ -251,14 +255,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
// send deposits confirmed message if applicable
maybeSendDepositsConfirmedMessages();
EasyBind.subscribe(trade.stateProperty(), state -> maybeSendDepositsConfirmedMessages());
}
public void maybeSendDepositsConfirmedMessages() {
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> {
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished()) return;
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
depositsConfirmedTasksCalled = true;
synchronized (trade) {
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
latchTrade();
@ -316,13 +320,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
MaybeSendSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
handleTaskRunnerFault(sender, request, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}
@ -354,13 +358,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
handleTaskRunnerFault(sender, message, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout
.executeTasks(true);
awaitTradeLatch();
} else {
@ -396,16 +400,16 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
.from(sender))
.setup(tasks(
// TODO (woodser): validate request
ProcessSignContractResponse.class)
SendDepositRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout(TRADE_TIMEOUT_SECONDS);
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
handleTaskRunnerFault(sender, message, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout
.executeTasks(true);
awaitTradeLatch();
} else {
@ -451,7 +455,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
errorMessage -> {
handleTaskRunnerFault(sender, response, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT_SECONDS))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}

View file

@ -64,7 +64,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// skip if payout tx already created
if (trade.getPayoutTxHex() != null) {
log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getId());
log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
complete();
return;
}
@ -83,7 +83,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
trade.importMultisigHex();
// create payout tx
log.info("Buyer creating unsigned payout tx");
log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());

View file

@ -28,6 +28,7 @@ import haveno.core.trade.Trade.State;
import haveno.core.trade.messages.SignContractRequest;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
@ -78,37 +79,70 @@ public class MaybeSendSignContractRequest extends TradeTask {
trade.addInitProgressStep();
// create deposit tx and freeze inputs
Integer subaddressIndex = null;
boolean reserveExactAmount = false;
if (trade instanceof MakerTrade) {
reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount();
if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
MoneroTxWallet depositTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
// collect relevant info
Integer subaddressIndex = null;
boolean reserveExactAmount = false;
if (trade instanceof MakerTrade) {
reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount();
if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
}
// thaw reserved outputs
if (trade.getSelf().getReserveTxKeyImages() != null) {
trade.getXmrWalletService().thawOutputs(trade.getSelf().getReserveTxKeyImages());
}
// attempt creating deposit tx
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) {
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (depositTx != null) break;
}
}
} catch (Exception e) {
// re-freeze reserved outputs
if (trade.getSelf().getReserveTxKeyImages() != null) {
trade.getXmrWalletService().freezeOutputs(trade.getSelf().getReserveTxKeyImages());
}
throw e;
}
// reset protocol timeout
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// update trade state
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee());
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
}
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
// check if trade still exists
if (!processModel.getTradeManager().hasOpenTrade(trade)) {
throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId());
}
// reset protocol timeout
trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS);
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save process state
trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee());
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
// TODO: security deposit should be based on trade amount, not max offer amount
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
// maker signs deposit hash nonce to avoid challenge protocol
byte[] sig = null;
@ -170,4 +204,8 @@ public class MaybeSendSignContractRequest extends TradeTask {
processModel.getTradeManager().requestPersistence();
complete();
}
private boolean isTimedOut() {
return !processModel.getTradeManager().hasOpenTrade(trade);
}
}

View file

@ -18,6 +18,7 @@
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;
@ -53,25 +54,26 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
// update multisig hex
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
}
// persist
processModel.getTradeManager().requestPersistence();
// update multisig hex
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// try to import multisig hex (retry later)
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
ThreadUtils.submitToPool(() -> {
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
});
// persist
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);

View file

@ -34,7 +34,6 @@
package haveno.core.trade.protocol.tasks;
import common.utils.GenUtils;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.sign.SignedWitness;
import haveno.core.support.dispute.Dispute;
@ -145,7 +144,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
for (int i = 0; i < 5; i++) {
if (trade.isPayoutPublished()) break;
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
}
if (!trade.isPayoutPublished()) trade.syncAndPollWallet();
}

View file

@ -60,15 +60,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
// if seller, decrypt buyer's payment account payload
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
trade.requestPersistence();
// try to import multisig hex off main thread (retry later)
new Thread(() -> {
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// update state
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);

View file

@ -37,7 +37,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
runInterceptHook();
// check connection
trade.checkAndVerifyDaemonConnection();
trade.verifyDaemonConnection();
// handle first time preparation
if (trade.getArbitrator().getPaymentReceivedMessage() == null) {

View file

@ -29,14 +29,16 @@ import haveno.core.trade.protocol.TradePeer;
import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Slf4j
public class ProcessSignContractResponse extends TradeTask {
public class SendDepositRequest extends TradeTask {
@SuppressWarnings({"unused"})
public ProcessSignContractResponse(TaskRunner taskHandler, Trade trade) {
public SendDepositRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -107,7 +109,11 @@ public class ProcessSignContractResponse extends TradeTask {
}
});
} else {
log.info("Waiting for another contract signature to send deposit request");
List<String> awaitingSignaturesFrom = new ArrayList<>();
if (processModel.getArbitrator().getContractSignature() == null) awaitingSignaturesFrom.add("arbitrator");
if (processModel.getMaker().getContractSignature() == null) awaitingSignaturesFrom.add("maker");
if (processModel.getTaker().getContractSignature() == null) awaitingSignaturesFrom.add("taker");
log.info("Waiting for contract signature from {} to send deposit request", awaitingSignaturesFrom);
complete(); // does not yet have needed signatures
}
} catch (Throwable t) {

View file

@ -23,6 +23,8 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet;
@ -30,6 +32,7 @@ import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class TakerReserveTradeFunds extends TradeTask {
public TakerReserveTradeFunds(TaskRunner taskHandler, Trade trade) {
@ -42,28 +45,49 @@ public class TakerReserveTradeFunds extends TradeTask {
runInterceptHook();
// create reserve tx
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
BigInteger takerFee = trade.getTakerFee();
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
// check if trade still exists
if (!processModel.getTradeManager().hasOpenTrade(trade)) {
throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId());
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
BigInteger takerFee = trade.getTakerFee();
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
// attempt creating reserve tx
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (reserveTx != null) break;
}
}
// reset protocol timeout
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// update trade state
trade.getTaker().setReserveTxKeyImages(reservedKeyImages);
}
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// reset protocol timeout
trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS);
// save process state
processModel.setReserveTx(reserveTx);
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
processModel.getTradeManager().requestPersistence();
trade.addInitProgressStep();
complete();
@ -74,4 +98,8 @@ public class TakerReserveTradeFunds extends TradeTask {
failed(t);
}
}
private boolean isTimedOut() {
return !processModel.getTradeManager().hasOpenTrade(trade);
}
}

View file

@ -22,7 +22,6 @@ import com.google.common.util.concurrent.Service.State;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import common.utils.GenUtils;
import common.utils.JsonUtils;
import haveno.common.ThreadUtils;
import haveno.common.UserThread;
@ -33,8 +32,6 @@ import haveno.common.util.Utilities;
import haveno.core.api.AccountServiceListener;
import haveno.core.api.CoreAccountService;
import haveno.core.api.XmrConnectionService;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOffer;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils;
@ -131,10 +128,10 @@ public class XmrWalletService {
private static final int NUM_MAX_WALLET_BACKUPS = 1;
private static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable
private static final int MAX_SYNC_ATTEMPTS = 3;
private static final boolean PRINT_STACK_TRACE = false;
private static final boolean PRINT_RPC_STACK_TRACE = false;
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long NUM_BLOCKS_BEHIND_WARNING = 10;
private static final long NUM_BLOCKS_BEHIND_WARNING = 5;
private final User user;
private final Preferences preferences;
@ -155,7 +152,7 @@ public class XmrWalletService {
private ChangeListener<? super Number> walletInitListener;
private TradeManager tradeManager;
private MoneroWallet wallet;
private Object walletLock = new Object();
public static final Object WALLET_LOCK = new Object();
private boolean wasWalletSynced = false;
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
private boolean isClosingWallet = false;
@ -374,11 +371,21 @@ public class XmrWalletService {
return useNativeXmrWallet && MoneroUtils.isNativeLibraryLoaded();
}
public MoneroSyncResult syncWallet() {
MoneroSyncResult result = syncWallet(wallet);
walletHeight.set(wallet.getHeight());
return result;
}
/**
* Sync the given wallet in a thread pool with other wallets.
*/
public MoneroSyncResult syncWallet(MoneroWallet wallet) {
Callable<MoneroSyncResult> task = () -> wallet.sync();
Callable<MoneroSyncResult> task = () -> {
synchronized (HavenoUtils.getDaemonLock()) {
return wallet.sync();
}
};
Future<MoneroSyncResult> future = syncWalletThreadPool.submit(task);
try {
return future.get();
@ -448,24 +455,26 @@ public class XmrWalletService {
if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name);
}
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
synchronized (walletLock) {
try {
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
//printTxs("XmrWalletService.createTx", tx);
requestSaveMainWallet();
return tx;
} catch (Exception e) {
throw e;
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (WALLET_LOCK) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
return wallet.createTx(txConfig);
}
}
}
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));;
//printTxs("XmrWalletService.createTx", tx);
requestSaveMainWallet();
return tx;
}
/**
* Thaw all outputs not reserved for a trade.
*/
public void thawUnreservedOutputs() {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
// collect reserved outputs
Set<String> reservedKeyImages = new HashSet<String>();
@ -505,26 +514,25 @@ public class XmrWalletService {
* @param keyImages the key images to freeze
*/
public void freezeOutputs(Collection<String> keyImages) {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
for (String keyImage : keyImages) wallet.freezeOutput(keyImage);
cacheWalletInfo();
requestSaveMainWallet();
doPollWallet(false);
}
updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output
}
/**
* Thaw the given outputs with a lock on the wallet.
*
* @param keyImages the key images to thaw
* @param keyImages the key images to thaw (ignored if null or empty)
*/
public void thawOutputs(Collection<String> keyImages) {
synchronized (walletLock) {
if (keyImages == null || keyImages.isEmpty()) return;
synchronized (WALLET_LOCK) {
for (String keyImage : keyImages) wallet.thawOutput(keyImage);
cacheWalletInfo();
requestSaveMainWallet();
doPollWallet(false);
}
updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output
}
private List<Integer> getSubaddressesWithExactInput(BigInteger amount) {
@ -542,40 +550,6 @@ public class XmrWalletService {
return new ArrayList<Integer>(subaddressIndices);
}
/**
* Create a reserve tx for an open offer and freeze its inputs.
*
* @param openOffer is the open offer to create a reserve tx for
*/
public MoneroTxWallet createReserveTx(OpenOffer openOffer) {
synchronized (walletLock) {
// collect offer data
Offer offer = openOffer.getOffer();
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct());
BigInteger makerFee = offer.getMaxMakerFee();
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
String returnAddress = getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
XmrAddressEntry fundingEntry = getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// create reserve tx
MoneroTxWallet reserveTx = createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save offer state
openOffer.setReserveTxHash(reserveTx.getHash());
openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
return reserveTx;
}
}
/**
* Create the reserve tx and freeze its inputs. The full amount is returned
* to the sender's payout address less the penalty and mining fees.
@ -587,14 +561,18 @@ public class XmrWalletService {
* @param returnAddress return address for reserved funds
* @param reserveExactAmount specifies to reserve the exact input amount
* @param preferredSubaddressIndex preferred source subaddress to spend from (optional)
* @return a transaction to reserve a trade
* @return the reserve tx
*/
public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis();
MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
return reserveTx;
synchronized (WALLET_LOCK) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis();
MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
return reserveTx;
}
}
}
/**
@ -606,28 +584,23 @@ public class XmrWalletService {
* @return MoneroTxWallet the multisig deposit tx
*/
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (walletLock) {
// thaw reserved outputs
if (trade.getSelf().getReserveTxKeyImages() != null) {
thawOutputs(trade.getSelf().getReserveTxKeyImages());
synchronized (WALLET_LOCK) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
String multisigAddress = trade.getProcessModel().getMultisigAddress();
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
long time = System.currentTimeMillis();
log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getShortId(), multisigAddress);
MoneroTxWallet depositTx = createTradeTx(null, tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getShortId(), System.currentTimeMillis() - time);
return depositTx;
}
// create deposit tx
String multisigAddress = trade.getProcessModel().getMultisigAddress();
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
long time = System.currentTimeMillis();
log.info("Creating deposit tx with multisig address={}", multisigAddress);
MoneroTxWallet depositTx = createTradeTx(null, tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time);
return depositTx;
}
}
private MoneroTxWallet createTradeTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
MoneroWallet wallet = getWallet();
// create a list of subaddresses to attempt spending from in preferred order
@ -675,7 +648,7 @@ public class XmrWalletService {
.setSubtractFeeFrom(0) // pay fee from transfer amount
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(HavenoUtils.getTradeFeeAddress(), feeAmount);
MoneroTxWallet tradeTx = wallet.createTx(txConfig);
MoneroTxWallet tradeTx = createTx(txConfig);
// freeze inputs
List<String> keyImages = new ArrayList<String>();
@ -872,7 +845,7 @@ public class XmrWalletService {
Runnable shutDownTask = () -> {
// remove listeners
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
if (wallet != null) {
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
wallet.removeListener(listener);
@ -1174,7 +1147,7 @@ public class XmrWalletService {
}
public List<MoneroTxWallet> getTxs() {
return getTxs(new MoneroTxQuery());
return getTxs(new MoneroTxQuery().setIncludeOutputs(true));
}
public List<MoneroTxWallet> getTxs(MoneroTxQuery query) {
@ -1242,7 +1215,7 @@ public class XmrWalletService {
// force restart main wallet if connection changed before synced
if (!wasWalletSynced) {
if (!Boolean.TRUE.equals(connection.isConnected())) return;
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return;
ThreadUtils.submitToPool(() -> {
log.warn("Force restarting main wallet because connection changed before inital sync");
forceRestartMainWallet();
@ -1275,7 +1248,7 @@ public class XmrWalletService {
private void initMainWalletIfConnected() {
ThreadUtils.execute(() -> {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) {
maybeInitMainWallet(true);
if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener);
@ -1295,7 +1268,7 @@ public class XmrWalletService {
}
private void maybeInitMainWallet(boolean sync, int numAttempts) {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
if (isShutDownStarted) return;
// open or create wallet main wallet
@ -1304,7 +1277,7 @@ public class XmrWalletService {
log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri()));
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced));
} else if (xmrConnectionService.getConnection() != null && Boolean.TRUE.equals(xmrConnectionService.getConnection().isConnected())) {
} else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) {
wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort);
// set wallet creation date to yesterday to guarantee complete restore
@ -1393,7 +1366,7 @@ public class XmrWalletService {
// get sync notifications from native wallet
if (wallet instanceof MoneroWalletFull) {
if (runReconnectTestOnStartup) GenUtils.waitFor(1000); // delay sync to test
if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
wallet.sync(new MoneroWalletListener() {
@Override
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
@ -1419,11 +1392,6 @@ public class XmrWalletService {
if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height);
else {
syncWithProgressLooper.stop();
try {
doPollWallet(true);
} catch (Exception e) {
e.printStackTrace();
}
wasWalletSynced = true;
updateSyncProgress(height);
syncWithProgressLatch.countDown();
@ -1465,19 +1433,19 @@ public class XmrWalletService {
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
// must be connected to daemon
MoneroRpcConnection connection = xmrConnectionService.getConnection();
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
// create wallet
MoneroWalletFull walletFull = null;
try {
// create wallet
MoneroRpcConnection connection = xmrConnectionService.getConnection();
log.info("Creating full wallet " + config.getPath() + " connected to monerod=" + connection.getUri());
long time = System.currentTimeMillis();
config.setServer(connection);
walletFull = MoneroWalletFull.createWallet(config);
walletFull.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done creating full wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
return walletFull;
} catch (Exception e) {
@ -1499,7 +1467,7 @@ public class XmrWalletService {
config.setNetworkType(getMoneroNetworkType());
config.setServer(connection);
walletFull = MoneroWalletFull.openWallet(config);
if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done opening full wallet " + config.getPath());
return walletFull;
} catch (Exception e) {
@ -1512,8 +1480,7 @@ public class XmrWalletService {
private MoneroWalletRpc createWalletRpc(MoneroWalletConfig config, Integer port) {
// must be connected to daemon
MoneroRpcConnection connection = xmrConnectionService.getConnection();
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
// create wallet
MoneroWalletRpc walletRpc = null;
@ -1521,17 +1488,18 @@ public class XmrWalletService {
// start monero-wallet-rpc instance
walletRpc = startWalletRpcInstance(port, isProxyApplied(false));
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE);
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
// prevent wallet rpc from syncing
walletRpc.stopSyncing();
// create wallet
MoneroRpcConnection connection = xmrConnectionService.getConnection();
log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri());
long time = System.currentTimeMillis();
config.setServer(connection);
walletRpc.createWallet(config);
walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
return walletRpc;
} catch (Exception e) {
@ -1547,7 +1515,7 @@ public class XmrWalletService {
// start monero-wallet-rpc instance
walletRpc = startWalletRpcInstance(port, applyProxyUri);
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE);
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
// prevent wallet rpc from syncing
walletRpc.stopSyncing();
@ -1560,7 +1528,7 @@ public class XmrWalletService {
log.info("Opening RPC wallet " + config.getPath() + " connected to daemon " + connection.getUri());
config.setServer(connection);
walletRpc.openWallet(config);
if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done opening RPC wallet " + config.getPath());
return walletRpc;
} catch (Exception e) {
@ -1613,7 +1581,7 @@ public class XmrWalletService {
}
private void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
if (wallet == null || isShutDownStarted) return;
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
@ -1634,7 +1602,7 @@ public class XmrWalletService {
// sync wallet on new thread
if (connection != null && !isShutDownStarted) {
wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
updatePollPeriod();
}
@ -1673,7 +1641,7 @@ public class XmrWalletService {
private void closeMainWallet(boolean save) {
stopPolling();
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
try {
if (wallet != null) {
isClosingWallet = true;
@ -1697,13 +1665,13 @@ public class XmrWalletService {
private void forceRestartMainWallet() {
log.warn("Force restarting main wallet");
forceCloseMainWallet();
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
maybeInitMainWallet(true);
}
}
private void startPolling() {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
if (isShutDownStarted || isPollInProgress()) return;
log.info("Starting to poll main wallet");
updatePollPeriod();
@ -1733,7 +1701,7 @@ public class XmrWalletService {
}
private void setPollPeriod(long pollPeriodMs) {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
if (this.isShutDownStarted) return;
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
this.pollPeriodMs = pollPeriodMs;
@ -1751,71 +1719,51 @@ public class XmrWalletService {
private void doPollWallet(boolean updateTxs) {
synchronized (pollLock) {
if (isShutDownStarted) return;
pollInProgress = true;
try {
// log warning if wallet is too far behind daemon
// switch to best connection if daemon is too far behind
MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo();
if (lastInfo == null) {
log.warn("Last daemon info is null");
return;
}
long walletHeight = wallet.getHeight();
if (wasWalletSynced && walletHeight < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) {
log.warn("Main wallet is {} blocks behind monerod, wallet height={}, monerod height={},", xmrConnectionService.getTargetHeight() - walletHeight, walletHeight, lastInfo.getHeight());
if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) {
log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight());
xmrConnectionService.switchToBestConnection();
}
// sync wallet if behind daemon
if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) wallet.sync();
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations
syncWallet();
}
}
// fetch transactions from pool and store to cache
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
if (updateTxs) {
try {
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
} catch (Exception e) { // fetch from pool can fail
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
synchronized (HavenoUtils.getDaemonLock()) {
try {
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
} catch (Exception e) { // fetch from pool can fail
if (!isShutDownStarted) log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
}
}
}
}
// get basic wallet info
long height = wallet.getHeight();
BigInteger balance = wallet.getBalance();
BigInteger unlockedBalance = wallet.getUnlockedBalance();
cachedSubaddresses = wallet.getSubaddresses(0);
cachedOutputs = wallet.getOutputs();
// cache and notify changes
if (cachedHeight == null) {
cachedHeight = height;
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
} else {
// notify listeners of new block
if (height != cachedHeight) {
cachedHeight = height;
onNewBlock(height);
}
// notify listeners of balance change
if (!balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance)) {
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
onBalancesChanged(balance, unlockedBalance);
}
}
// cache wallet info
cacheWalletInfo();
} catch (Exception e) {
if (isShutDownStarted) return;
if (wallet == null || isShutDownStarted) return;
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
if (isConnectionRefused && wallet != null) forceRestartMainWallet();
else {
boolean isWalletConnected = isWalletConnectedToDaemon();
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected
if (wallet != null && isWalletConnected) {
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection());
//e.printStackTrace();
}
if (isConnectionRefused) forceRestartMainWallet();
else if (isWalletConnectedToDaemon()) {
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection());
//e.printStackTrace();
}
} finally {
pollInProgress = false;
@ -1824,7 +1772,7 @@ public class XmrWalletService {
}
public boolean isWalletConnectedToDaemon() {
synchronized (walletLock) {
synchronized (WALLET_LOCK) {
try {
if (wallet == null) return false;
return wallet.isConnectedToDaemon();
@ -1841,6 +1789,33 @@ public class XmrWalletService {
});
}
private void cacheWalletInfo() {
// get basic wallet info
long height = wallet.getHeight();
BigInteger balance = wallet.getBalance();
BigInteger unlockedBalance = wallet.getUnlockedBalance();
cachedSubaddresses = wallet.getSubaddresses(0);
cachedOutputs = wallet.getOutputs();
// cache and notify changes
if (cachedHeight == null) {
cachedHeight = height;
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
onNewBlock(height);
onBalancesChanged(balance, unlockedBalance);
} else {
boolean heightChanged = height != cachedHeight;
boolean balancesChanged = !balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance);
cachedHeight = height;
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
if (heightChanged) onNewBlock(height);
if (balancesChanged) onBalancesChanged(balance, unlockedBalance);
}
}
private void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
updateBalanceListeners();
for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance));

View file

@ -57,10 +57,10 @@ import haveno.proto.grpc.SetAutoSwitchReply;
import haveno.proto.grpc.SetAutoSwitchRequest;
import haveno.proto.grpc.SetConnectionReply;
import haveno.proto.grpc.SetConnectionRequest;
import haveno.proto.grpc.StartCheckingConnectionsReply;
import haveno.proto.grpc.StartCheckingConnectionsRequest;
import haveno.proto.grpc.StopCheckingConnectionsReply;
import haveno.proto.grpc.StopCheckingConnectionsRequest;
import haveno.proto.grpc.StartCheckingConnectionReply;
import haveno.proto.grpc.StartCheckingConnectionRequest;
import haveno.proto.grpc.StopCheckingConnectionReply;
import haveno.proto.grpc.StopCheckingConnectionRequest;
import haveno.proto.grpc.UrlConnection;
import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase;
import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod;
@ -72,8 +72,8 @@ import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionsMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getSetAutoSwitchMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getSetConnectionMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionsMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionsMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionMethod;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
import java.net.MalformedURLException;
@ -102,7 +102,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void addConnection(AddConnectionRequest request,
StreamObserver<AddConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.addMoneroConnection(toMoneroRpcConnection(request.getConnection()));
coreApi.addXmrConnection(toMoneroRpcConnection(request.getConnection()));
return AddConnectionReply.newBuilder().build();
});
}
@ -111,7 +111,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void removeConnection(RemoveConnectionRequest request,
StreamObserver<RemoveConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.removeMoneroConnection(validateUri(request.getUrl()));
coreApi.removeXmrConnection(validateUri(request.getUrl()));
return RemoveConnectionReply.newBuilder().build();
});
}
@ -120,7 +120,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void getConnection(GetConnectionRequest request,
StreamObserver<GetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection());
UrlConnection replyConnection = toUrlConnection(coreApi.getXmrConnection());
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
if (replyConnection != null) {
builder.setConnection(replyConnection);
@ -145,10 +145,10 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
StreamObserver<SetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
if (request.getUrl() != null && !request.getUrl().isEmpty())
coreApi.setMoneroConnection(validateUri(request.getUrl()));
coreApi.setXmrConnection(validateUri(request.getUrl()));
else if (request.hasConnection())
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection()));
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client
coreApi.setXmrConnection(toMoneroRpcConnection(request.getConnection()));
else coreApi.setXmrConnection((MoneroRpcConnection) null); // disconnect from client
return SetConnectionReply.newBuilder().build();
});
}
@ -157,7 +157,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void checkConnection(CheckConnectionRequest request,
StreamObserver<CheckConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.checkMoneroConnection();
MoneroRpcConnection connection = coreApi.checkXmrConnection();
UrlConnection replyConnection = toUrlConnection(connection);
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
if (replyConnection != null) {
@ -179,22 +179,22 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
}
@Override
public void startCheckingConnections(StartCheckingConnectionsRequest request,
StreamObserver<StartCheckingConnectionsReply> responseObserver) {
public void startCheckingConnection(StartCheckingConnectionRequest request,
StreamObserver<StartCheckingConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
int refreshMillis = request.getRefreshPeriod();
Long refreshPeriod = refreshMillis == 0 ? null : (long) refreshMillis;
coreApi.startCheckingMoneroConnection(refreshPeriod);
return StartCheckingConnectionsReply.newBuilder().build();
coreApi.startCheckingXmrConnection(refreshPeriod);
return StartCheckingConnectionReply.newBuilder().build();
});
}
@Override
public void stopCheckingConnections(StopCheckingConnectionsRequest request,
StreamObserver<StopCheckingConnectionsReply> responseObserver) {
public void stopCheckingConnection(StopCheckingConnectionRequest request,
StreamObserver<StopCheckingConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.stopCheckingMoneroConnection();
return StopCheckingConnectionsReply.newBuilder().build();
coreApi.stopCheckingXmrConnection();
return StopCheckingConnectionReply.newBuilder().build();
});
}
@ -202,7 +202,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void getBestAvailableConnection(GetBestAvailableConnectionRequest request,
StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection();
MoneroRpcConnection connection = coreApi.getBestAvailableXmrConnection();
UrlConnection replyConnection = toUrlConnection(connection);
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
if (replyConnection != null) {
@ -216,7 +216,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void setAutoSwitch(SetAutoSwitchRequest request,
StreamObserver<SetAutoSwitchReply> responseObserver) {
handleRequest(responseObserver, () -> {
coreApi.setMoneroConnectionAutoSwitch(request.getAutoSwitch());
coreApi.setXmrConnectionAutoSwitch(request.getAutoSwitch());
return SetAutoSwitchReply.newBuilder().build();
});
}
@ -300,8 +300,8 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStartCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStopCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStartCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStopCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
}}

View file

@ -91,6 +91,7 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import monero.common.MoneroUtils;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroWalletListener;
import net.glxn.qrgen.QRCode;
@ -365,7 +366,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
@NotNull
private String getPaymentUri() {
return xmrWalletService.getWallet().getPaymentUri(new MoneroTxConfig()
return MoneroUtils.getPaymentUri(new MoneroTxConfig()
.setAddress(addressTextField.getAddress())
.setAmount(HavenoUtils.coinToAtomicUnits(getAmount()))
.setNote(paymentLabelString));

View file

@ -261,7 +261,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// create tx
if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow"));
MoneroTxWallet tx = xmrWalletService.getWallet().createTx(new MoneroTxConfig()
MoneroTxWallet tx = xmrWalletService.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setAmount(amount)
.setAddress(withdrawToAddress)

View file

@ -613,10 +613,12 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo"));
else errorMessage.set(errMessage);
updateButtonDisableState();
updateSpinnerInfo();
UserThread.execute(() -> {
updateButtonDisableState();
updateSpinnerInfo();
resultHandler.run();
resultHandler.run();
});
});
updateButtonDisableState();

View file

@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import haveno.common.ClockWatcher;
import haveno.common.UserThread;
import haveno.common.app.DevEnv;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.network.MessageState;
@ -101,6 +102,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
@Getter
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private Subscription tradeStateSubscription;
private Subscription paymentAccountDecryptedSubscription;
private Subscription payoutStateSubscription;
private Subscription messageStateSubscription;
@Getter
@ -146,6 +148,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = null;
}
if (paymentAccountDecryptedSubscription != null) {
paymentAccountDecryptedSubscription.unsubscribe();
paymentAccountDecryptedSubscription = null;
}
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
payoutStateSubscription = null;
@ -167,6 +174,10 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
buyerState.set(BuyerState.UNDEFINED);
}
if (paymentAccountDecryptedSubscription != null) {
paymentAccountDecryptedSubscription.unsubscribe();
}
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
sellerState.set(SellerState.UNDEFINED);
@ -183,6 +194,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
onTradeStateChanged(state);
});
paymentAccountDecryptedSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentAccountDecryptedProperty(), decrypted -> {
refresh();
});
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
onPayoutStateChanged(state);
});
@ -191,6 +205,14 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
}
}
private void refresh() {
UserThread.execute(() -> {
sellerState.set(UNDEFINED);
buyerState.set(BuyerState.UNDEFINED);
onTradeStateChanged(trade.getState());
});
}
private void onMessageStateChanged(MessageState messageState) {
messageStateProperty.set(messageState);
}

View file

@ -221,7 +221,7 @@ public class BuyerStep2View extends TradeStepView {
PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "<missing payment account payload>";
String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "<pending>";
TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4,
Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)),
Layout.COMPACT_GROUP_DISTANCE);

View file

@ -93,6 +93,7 @@ import javafx.stage.StageStyle;
import javafx.util.Callback;
import javafx.util.StringConverter;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroUtils;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxConfig;
@ -686,7 +687,7 @@ public class GUIUtil {
}
public static String getMoneroURI(String address, BigInteger amount, String label, MoneroWallet wallet) {
return wallet.getPaymentUri(new MoneroTxConfig()
return MoneroUtils.getPaymentUri(new MoneroTxConfig()
.setAddress(address)
.setAmount(amount)
.setNote(label));

View file

@ -315,9 +315,9 @@ service XmrConnections {
}
rpc CheckConnections(CheckConnectionsRequest) returns (CheckConnectionsReply) {
}
rpc StartCheckingConnections(StartCheckingConnectionsRequest) returns (StartCheckingConnectionsReply) {
rpc StartCheckingConnection(StartCheckingConnectionRequest) returns (StartCheckingConnectionReply) {
}
rpc StopCheckingConnections(StopCheckingConnectionsRequest) returns (StopCheckingConnectionsReply) {
rpc StopCheckingConnection(StopCheckingConnectionRequest) returns (StopCheckingConnectionReply) {
}
rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) {
}
@ -388,15 +388,15 @@ message CheckConnectionsReply {
repeated UrlConnection connections = 1;
}
message StartCheckingConnectionsRequest {
message StartCheckingConnectionRequest {
int32 refresh_period = 1; // milliseconds
}
message StartCheckingConnectionsReply {}
message StartCheckingConnectionReply {}
message StopCheckingConnectionsRequest {}
message StopCheckingConnectionRequest {}
message StopCheckingConnectionsReply {}
message StopCheckingConnectionReply {}
message GetBestAvailableConnectionRequest {}