mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-11-17 08:17:57 +00:00
stability fixes on tor
optimize when multisig info imported fetch updates for tx progress indicators off main thread add synchronization locks refactor address entry management add totalTxFee to process model prevent same user from taking same offer at same time set refresh rate to 30s for tor
This commit is contained in:
parent
36cf91e093
commit
1b753e4f29
33 changed files with 498 additions and 354 deletions
|
@ -44,7 +44,8 @@ public final class CoreMoneroConnectionsService {
|
||||||
|
|
||||||
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
|
private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet
|
||||||
private static final long REFRESH_PERIOD_LOCAL_MS = 5000; // refresh period when connected to local node
|
private static final long REFRESH_PERIOD_LOCAL_MS = 5000; // refresh period when connected to local node
|
||||||
private static final long REFRESH_PERIOD_REMOTE_MS = 20000; // refresh period when connected to remote node
|
private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http
|
||||||
|
private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor
|
||||||
private static final long MIN_ERROR_LOG_PERIOD_MS = 300000; // minimum period between logging errors fetching daemon info
|
private static final long MIN_ERROR_LOG_PERIOD_MS = 300000; // minimum period between logging errors fetching daemon info
|
||||||
private static Long lastErrorTimestamp;
|
private static Long lastErrorTimestamp;
|
||||||
|
|
||||||
|
@ -157,6 +158,10 @@ public final class CoreMoneroConnectionsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isConnected() {
|
||||||
|
return connectionManager.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
public void addConnection(MoneroRpcConnection connection) {
|
public void addConnection(MoneroRpcConnection connection) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
accountService.checkAccountOpen();
|
accountService.checkAccountOpen();
|
||||||
|
@ -256,10 +261,12 @@ public final class CoreMoneroConnectionsService {
|
||||||
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
|
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
|
||||||
else {
|
else {
|
||||||
if (isConnectionLocal()) {
|
if (isConnectionLocal()) {
|
||||||
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped
|
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
|
||||||
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||||
|
} else if (getConnection().isOnion()) {
|
||||||
|
return REFRESH_PERIOD_ONION_MS;
|
||||||
} else {
|
} else {
|
||||||
return REFRESH_PERIOD_REMOTE_MS;
|
return REFRESH_PERIOD_HTTP_MS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -327,7 +334,7 @@ public final class CoreMoneroConnectionsService {
|
||||||
|
|
||||||
// reset connection manager
|
// reset connection manager
|
||||||
connectionManager.reset();
|
connectionManager.reset();
|
||||||
connectionManager.setTimeout(REFRESH_PERIOD_REMOTE_MS);
|
connectionManager.setTimeout(REFRESH_PERIOD_HTTP_MS);
|
||||||
|
|
||||||
// load connections
|
// load connections
|
||||||
log.info("TOR proxy URI: " + getProxyUri());
|
log.info("TOR proxy URI: " + getProxyUri());
|
||||||
|
|
|
@ -107,7 +107,9 @@ public class OfferBookService {
|
||||||
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
|
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
||||||
protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> {
|
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||||
|
synchronized (offerBookChangedListeners) {
|
||||||
|
offerBookChangedListeners.forEach(listener -> {
|
||||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||||
maybeInitializeKeyImagePoller();
|
maybeInitializeKeyImagePoller();
|
||||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||||
|
@ -117,12 +119,16 @@ public class OfferBookService {
|
||||||
setReservedFundsSpent(offer);
|
setReservedFundsSpent(offer);
|
||||||
listener.onAdded(offer);
|
listener.onAdded(offer);
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
|
||||||
protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> {
|
protectedStorageEntries.forEach(protectedStorageEntry -> {
|
||||||
|
synchronized (offerBookChangedListeners) {
|
||||||
|
offerBookChangedListeners.forEach(listener -> {
|
||||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||||
maybeInitializeKeyImagePoller();
|
maybeInitializeKeyImagePoller();
|
||||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||||
|
@ -132,7 +138,9 @@ public class OfferBookService {
|
||||||
setReservedFundsSpent(offer);
|
setReservedFundsSpent(offer);
|
||||||
listener.onRemoved(offer);
|
listener.onRemoved(offer);
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -244,8 +252,10 @@ public class OfferBookService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addOfferBookChangedListener(OfferBookChangedListener offerBookChangedListener) {
|
public void addOfferBookChangedListener(OfferBookChangedListener offerBookChangedListener) {
|
||||||
|
synchronized (offerBookChangedListeners) {
|
||||||
offerBookChangedListeners.add(offerBookChangedListener);
|
offerBookChangedListeners.add(offerBookChangedListener);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -280,6 +290,7 @@ public class OfferBookService {
|
||||||
private void updateAffectedOffers(String keyImage) {
|
private void updateAffectedOffers(String keyImage) {
|
||||||
for (Offer offer : getOffers()) {
|
for (Offer offer : getOffers()) {
|
||||||
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) {
|
||||||
|
synchronized (offerBookChangedListeners) {
|
||||||
offerBookChangedListeners.forEach(listener -> {
|
offerBookChangedListeners.forEach(listener -> {
|
||||||
listener.onRemoved(offer);
|
listener.onRemoved(offer);
|
||||||
listener.onAdded(offer);
|
listener.onAdded(offer);
|
||||||
|
@ -287,6 +298,7 @@ public class OfferBookService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setReservedFundsSpent(Offer offer) {
|
private void setReservedFundsSpent(Offer offer) {
|
||||||
if (keyImagePoller == null) return;
|
if (keyImagePoller == null) return;
|
||||||
|
|
|
@ -984,6 +984,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
||||||
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
||||||
Tuple2<MoneroTx, BigInteger> txResult = xmrWalletService.verifyTradeTx(
|
Tuple2<MoneroTx, BigInteger> txResult = xmrWalletService.verifyTradeTx(
|
||||||
|
offer.getId(),
|
||||||
tradeFee,
|
tradeFee,
|
||||||
sendAmount,
|
sendAmount,
|
||||||
securityDeposit,
|
securityDeposit,
|
||||||
|
@ -1168,9 +1169,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId,
|
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId,
|
||||||
availabilityResult,
|
availabilityResult,
|
||||||
makerSignature);
|
makerSignature);
|
||||||
log.info("Send {} with offerId {} and uid {} to peer {}",
|
log.info("Send {} with offerId {}, uid {}, and result {} to peer {}",
|
||||||
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(),
|
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(),
|
||||||
offerAvailabilityResponse.getUid(), peer);
|
offerAvailabilityResponse.getUid(),
|
||||||
|
availabilityResult,
|
||||||
|
peer);
|
||||||
p2PService.sendEncryptedDirectMessage(peer,
|
p2PService.sendEncryptedDirectMessage(peer,
|
||||||
request.getPubKeyRing(),
|
request.getPubKeyRing(),
|
||||||
offerAvailabilityResponse,
|
offerAvailabilityResponse,
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
|
||||||
XmrWalletService walletService = model.getXmrWalletService();
|
XmrWalletService walletService = model.getXmrWalletService();
|
||||||
String paymentAccountId = model.getPaymentAccountId();
|
String paymentAccountId = model.getPaymentAccountId();
|
||||||
String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
|
String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId();
|
||||||
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // reserve new payout address
|
String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
|
|
||||||
// taker signs offer using offer id as nonce to avoid challenge protocol
|
// taker signs offer using offer id as nonce to avoid challenge protocol
|
||||||
byte[] sig = HavenoUtils.sign(model.getP2PService().getKeyRing(), offer.getId());
|
byte[] sig = HavenoUtils.sign(model.getP2PService().getKeyRing(), offer.getId());
|
||||||
|
|
|
@ -53,9 +53,15 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
BigInteger makerFee = offer.getMakerFee();
|
BigInteger makerFee = offer.getMakerFee();
|
||||||
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
||||||
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress);
|
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress);
|
||||||
|
|
||||||
|
// 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 reserved key images
|
// collect reserved key images
|
||||||
List<String> reservedKeyImages = new ArrayList<String>();
|
List<String> reservedKeyImages = new ArrayList<String>();
|
||||||
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// create request for arbitrator to sign offer
|
// create request for arbitrator to sign offer
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
||||||
SignOfferRequest request = new SignOfferRequest(
|
SignOfferRequest request = new SignOfferRequest(
|
||||||
model.getOffer().getId(),
|
model.getOffer().getId(),
|
||||||
P2PService.getMyNodeAddress(),
|
P2PService.getMyNodeAddress(),
|
||||||
|
|
|
@ -96,7 +96,7 @@ public class TakeOfferModel implements Model {
|
||||||
this.clearModel();
|
this.clearModel();
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.paymentAccount = paymentAccount;
|
this.paymentAccount = paymentAccount;
|
||||||
this.addressEntry = xmrWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); // TODO (woodser): replace with xmr or remove
|
this.addressEntry = xmrWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING);
|
||||||
validateModelInputs();
|
validateModelInputs();
|
||||||
|
|
||||||
this.useSavingsWallet = useSavingsWallet;
|
this.useSavingsWallet = useSavingsWallet;
|
||||||
|
|
|
@ -99,7 +99,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable<Payme
|
||||||
// Static
|
// Static
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// time in ms for 1 day (mainnet), 30m (stagenet) or 1 minute (local)
|
// time in ms for 1 "day" (mainnet), 30m (stagenet) or 1 minute (local)
|
||||||
private static final long DAY = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? TimeUnit.MINUTES.toMillis(1) :
|
private static final long DAY = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? TimeUnit.MINUTES.toMillis(1) :
|
||||||
Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) :
|
Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_STAGENET ? TimeUnit.MINUTES.toMillis(30) :
|
||||||
TimeUnit.DAYS.toMillis(1);
|
TimeUnit.DAYS.toMillis(1);
|
||||||
|
|
|
@ -827,7 +827,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
if (!trade.isPayoutPublished()) {
|
if (!trade.isPayoutPublished()) {
|
||||||
|
|
||||||
// create unsigned dispute payout tx
|
// create unsigned dispute payout tx
|
||||||
log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId());
|
log.info("Creating unsigned dispute payout tx for trade {}", trade.getId());
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// trade wallet must be synced
|
// trade wallet must be synced
|
||||||
|
|
|
@ -471,17 +471,25 @@ public class HavenoUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void executeTasks(Collection<Runnable> tasks, int maxConcurrency) {
|
public static void executeTasks(Collection<Runnable> tasks, int maxConcurrency) {
|
||||||
|
executeTasks(tasks, maxConcurrency, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void executeTasks(Collection<Runnable> tasks, int maxConcurrency, Long timeoutSeconds) {
|
||||||
if (tasks.isEmpty()) return;
|
if (tasks.isEmpty()) return;
|
||||||
ExecutorService pool = Executors.newFixedThreadPool(maxConcurrency);
|
ExecutorService pool = Executors.newFixedThreadPool(maxConcurrency);
|
||||||
List<Future<?>> futures = new ArrayList<Future<?>>();
|
List<Future<?>> futures = new ArrayList<Future<?>>();
|
||||||
for (Runnable task : tasks) futures.add(pool.submit(task));
|
for (Runnable task : tasks) futures.add(pool.submit(task));
|
||||||
pool.shutdown();
|
pool.shutdown();
|
||||||
|
|
||||||
|
// interrupt after timeout
|
||||||
|
if (timeoutSeconds != null) {
|
||||||
try {
|
try {
|
||||||
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) pool.shutdownNow();
|
if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) pool.shutdownNow();
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
pool.shutdownNow();
|
pool.shutdownNow();
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// throw exception from any tasks
|
// throw exception from any tasks
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -317,6 +317,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
@Getter
|
@Getter
|
||||||
private final Offer offer;
|
private final Offer offer;
|
||||||
private final long takerFee;
|
private final long takerFee;
|
||||||
|
private final long totalTxFee;
|
||||||
|
|
||||||
// Added in 1.5.1
|
// Added in 1.5.1
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -362,8 +363,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
// Transient
|
// Transient
|
||||||
// Immutable
|
// Immutable
|
||||||
@Getter
|
@Getter
|
||||||
transient final private BigInteger totalTxFee;
|
|
||||||
@Getter
|
|
||||||
transient final private XmrWalletService xmrWalletService;
|
transient final private XmrWalletService xmrWalletService;
|
||||||
|
|
||||||
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||||
|
@ -385,6 +384,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
// Mutable
|
// Mutable
|
||||||
@Getter
|
@Getter
|
||||||
transient private boolean isInitialized;
|
transient private boolean isInitialized;
|
||||||
|
@Getter
|
||||||
transient private boolean isShutDown;
|
transient private boolean isShutDown;
|
||||||
|
|
||||||
// Added in v1.2.0
|
// Added in v1.2.0
|
||||||
|
@ -465,7 +465,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.amount = tradeAmount.longValueExact();
|
this.amount = tradeAmount.longValueExact();
|
||||||
this.takerFee = takerFee.longValueExact();
|
this.takerFee = takerFee.longValueExact();
|
||||||
this.totalTxFee = BigInteger.valueOf(0); // TODO: sum tx fees
|
this.totalTxFee = 0l; // TODO: sum tx fees
|
||||||
this.price = tradePrice;
|
this.price = tradePrice;
|
||||||
this.xmrWalletService = xmrWalletService;
|
this.xmrWalletService = xmrWalletService;
|
||||||
this.processModel = processModel;
|
this.processModel = processModel;
|
||||||
|
@ -585,6 +585,18 @@ public abstract class Trade implements Tradable, Model {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset payment sent state if no ack receive
|
||||||
|
if (getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
|
||||||
|
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||||
|
setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset payment received state if no ack receive
|
||||||
|
if (getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||||
|
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||||
|
setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||||
|
}
|
||||||
|
|
||||||
// handle trade state events
|
// handle trade state events
|
||||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||||
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
||||||
|
@ -621,10 +633,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (!isInitialized) return;
|
if (!isInitialized) return;
|
||||||
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId());
|
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId());
|
||||||
deleteWallet();
|
deleteWallet();
|
||||||
if (txPollLooper != null) {
|
|
||||||
txPollLooper.stop();
|
|
||||||
txPollLooper = null;
|
|
||||||
}
|
|
||||||
if (idlePayoutSyncer != null) {
|
if (idlePayoutSyncer != null) {
|
||||||
xmrWalletService.removeWalletListener(idlePayoutSyncer);
|
xmrWalletService.removeWalletListener(idlePayoutSyncer);
|
||||||
idlePayoutSyncer = null;
|
idlePayoutSyncer = null;
|
||||||
|
@ -702,6 +710,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (wallet != null) return wallet;
|
if (wallet != null) return wallet;
|
||||||
if (!walletExists()) return null;
|
if (!walletExists()) return null;
|
||||||
|
if (isShutDown) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because trade is shut down");
|
||||||
if (!isShutDown) wallet = xmrWalletService.openWallet(getWalletName());
|
if (!isShutDown) wallet = xmrWalletService.openWallet(getWalletName());
|
||||||
return wallet;
|
return wallet;
|
||||||
}
|
}
|
||||||
|
@ -746,7 +755,6 @@ public abstract class Trade implements Tradable, Model {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!isShutDown) {
|
if (!isShutDown) {
|
||||||
log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -784,6 +792,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
private void closeWallet() {
|
private void closeWallet() {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId());
|
if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId());
|
||||||
|
stopPolling();
|
||||||
xmrWalletService.closeWallet(wallet, true);
|
xmrWalletService.closeWallet(wallet, true);
|
||||||
wallet = null;
|
wallet = null;
|
||||||
}
|
}
|
||||||
|
@ -977,10 +986,21 @@ public abstract class Trade implements Tradable, Model {
|
||||||
if (sign) {
|
if (sign) {
|
||||||
|
|
||||||
// sign tx
|
// sign tx
|
||||||
|
try {
|
||||||
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
|
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
||||||
payoutTxHex = result.getSignedMultisigTxHex();
|
payoutTxHex = result.getSignedMultisigTxHex();
|
||||||
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); // update described set
|
} catch (Exception e) {
|
||||||
|
if (getPayoutTxHex() != null) {
|
||||||
|
log.info("Reusing previous payout tx for {} {} because signing failed with error \"{}\"", getClass().getSimpleName(), getId(), e.getMessage()); // in case previous message with signed tx failed to send
|
||||||
|
payoutTxHex = getPayoutTxHex();
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// describe result
|
||||||
|
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
|
||||||
payoutTx = describedTxSet.getTxs().get(0);
|
payoutTx = describedTxSet.getTxs().get(0);
|
||||||
|
|
||||||
// verify fee is within tolerance by recreating payout tx
|
// verify fee is within tolerance by recreating payout tx
|
||||||
|
@ -1049,6 +1069,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
|
|
||||||
private MoneroTx getDepositTx(TradePeer trader) {
|
private MoneroTx getDepositTx(TradePeer trader) {
|
||||||
String depositId = trader.getDepositTxHash();
|
String depositId = trader.getDepositTxHash();
|
||||||
|
if (depositId == null) return null;
|
||||||
try {
|
try {
|
||||||
if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) {
|
if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) {
|
||||||
trader.setDepositTx(getTxFromWalletOrDaemon(depositId));
|
trader.setDepositTx(getTxFromWalletOrDaemon(depositId));
|
||||||
|
@ -1106,21 +1127,18 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown() {
|
public void shutDown() {
|
||||||
synchronized (walletLock) {
|
synchronized (this) {
|
||||||
log.info("Shutting down {} {}", getClass().getSimpleName(), getId());
|
log.info("Shutting down {} {}", getClass().getSimpleName(), getId());
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
isShutDown = true;
|
isShutDown = true;
|
||||||
if (wallet != null) closeWallet();
|
if (wallet != null) closeWallet();
|
||||||
if (txPollLooper != null) {
|
|
||||||
txPollLooper.stop();
|
|
||||||
txPollLooper = null;
|
|
||||||
}
|
|
||||||
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
|
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
|
||||||
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
|
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
|
||||||
if (idlePayoutSyncer != null) {
|
if (idlePayoutSyncer != null) {
|
||||||
xmrWalletService.removeWalletListener(idlePayoutSyncer);
|
xmrWalletService.removeWalletListener(idlePayoutSyncer);
|
||||||
idlePayoutSyncer = null;
|
idlePayoutSyncer = null;
|
||||||
}
|
}
|
||||||
|
log.info("Done shutting down {} {}", getClass().getSimpleName(), getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1558,6 +1576,10 @@ public abstract class Trade implements Tradable, Model {
|
||||||
return BigInteger.valueOf(takerFee);
|
return BigInteger.valueOf(takerFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BigInteger getTotalTxFee() {
|
||||||
|
return BigInteger.valueOf(totalTxFee);
|
||||||
|
}
|
||||||
|
|
||||||
public BigInteger getBuyerSecurityDeposit() {
|
public BigInteger getBuyerSecurityDeposit() {
|
||||||
if (getBuyer().getDepositTxHash() == null) return null;
|
if (getBuyer().getDepositTxHash() == null) return null;
|
||||||
return getBuyer().getSecurityDeposit();
|
return getBuyer().getSecurityDeposit();
|
||||||
|
@ -1621,10 +1643,13 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
if (isShutDown) return;
|
||||||
MoneroWallet wallet = getWallet();
|
MoneroWallet wallet = getWallet();
|
||||||
if (wallet == null) return;
|
if (wallet == null) return;
|
||||||
log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri());
|
log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri());
|
||||||
wallet.setDaemonConnection(connection);
|
wallet.setDaemonConnection(connection);
|
||||||
|
updateWalletRefreshPeriod();
|
||||||
|
|
||||||
// sync and reprocess messages on new thread
|
// sync and reprocess messages on new thread
|
||||||
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
||||||
|
@ -1637,18 +1662,19 @@ public abstract class Trade implements Tradable, Model {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSyncing() {
|
private void updateSyncing() {
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
if (!isIdling()) {
|
if (!isIdling()) {
|
||||||
trySyncWallet();
|
|
||||||
updateWalletRefreshPeriod();
|
updateWalletRefreshPeriod();
|
||||||
|
trySyncWallet();
|
||||||
} else {
|
} else {
|
||||||
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
if (!isShutDown) {
|
if (!isShutDown) {
|
||||||
trySyncWallet();
|
|
||||||
updateWalletRefreshPeriod();
|
updateWalletRefreshPeriod();
|
||||||
|
trySyncWallet();
|
||||||
}
|
}
|
||||||
}, startSyncingInMs / 1000l);
|
}, startSyncingInMs / 1000l);
|
||||||
}
|
}
|
||||||
|
@ -1659,28 +1685,36 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
|
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
|
||||||
|
synchronized (walletLock) {
|
||||||
if (this.isShutDown) return;
|
if (this.isShutDown) return;
|
||||||
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
|
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
|
||||||
this.walletRefreshPeriod = walletRefreshPeriod;
|
this.walletRefreshPeriod = walletRefreshPeriod;
|
||||||
synchronized (walletLock) {
|
|
||||||
if (getWallet() != null) {
|
if (getWallet() != null) {
|
||||||
log.info("Setting wallet refresh rate for {} {} to {}", getClass().getSimpleName(), getId(), walletRefreshPeriod);
|
log.info("Setting wallet refresh rate for {} {} to {}", getClass().getSimpleName(), getId(), walletRefreshPeriod);
|
||||||
getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period
|
getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period
|
||||||
}
|
}
|
||||||
if (txPollLooper != null) {
|
stopPolling();
|
||||||
txPollLooper.stop();
|
|
||||||
txPollLooper = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
startPolling();
|
startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startPolling() {
|
private void startPolling() {
|
||||||
|
synchronized (walletLock) {
|
||||||
if (txPollLooper != null) return;
|
if (txPollLooper != null) return;
|
||||||
log.info("Listening for payout tx for {} {}", getClass().getSimpleName(), getId());
|
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
txPollLooper = new TaskLooper(() -> { pollWallet(); });
|
txPollLooper = new TaskLooper(() -> { pollWallet(); });
|
||||||
txPollLooper.start(walletRefreshPeriod);
|
txPollLooper.start(walletRefreshPeriod);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopPolling() {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
if (txPollLooper != null) {
|
||||||
|
txPollLooper.stop();
|
||||||
|
txPollLooper = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void pollWallet() {
|
private void pollWallet() {
|
||||||
try {
|
try {
|
||||||
|
@ -1698,6 +1732,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
|
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
|
||||||
.setIncludeOutputs(true));
|
.setIncludeOutputs(true));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (!isShutDown) log.info("Could not fetch deposit txs from wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); // expected at first
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1750,7 +1785,10 @@ public abstract class Trade implements Tradable, Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!isShutDown && getWallet() != null && isWalletConnected()) log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage());
|
if (!isShutDown && getWallet() != null && isWalletConnected()) {
|
||||||
|
log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1844,6 +1882,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
|
protobuf.Trade.Builder builder = protobuf.Trade.newBuilder()
|
||||||
.setOffer(offer.toProtoMessage())
|
.setOffer(offer.toProtoMessage())
|
||||||
.setTakerFee(takerFee)
|
.setTakerFee(takerFee)
|
||||||
|
.setTotalTxFee(totalTxFee)
|
||||||
.setTakeOfferDate(takeOfferDate)
|
.setTakeOfferDate(takeOfferDate)
|
||||||
.setProcessModel(processModel.toProtoMessage())
|
.setProcessModel(processModel.toProtoMessage())
|
||||||
.setAmount(amount)
|
.setAmount(amount)
|
||||||
|
@ -1868,7 +1907,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
|
Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState)));
|
||||||
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
|
Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState)));
|
||||||
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
|
||||||
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey));
|
Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey));
|
||||||
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
|
||||||
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
|
Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name()));
|
||||||
return builder.build();
|
return builder.build();
|
||||||
|
@ -1913,6 +1952,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
return "Trade{" +
|
return "Trade{" +
|
||||||
"\n offer=" + offer +
|
"\n offer=" + offer +
|
||||||
",\n takerFee=" + takerFee +
|
",\n takerFee=" + takerFee +
|
||||||
|
",\n totalTxFee=" + totalTxFee +
|
||||||
",\n takeOfferDate=" + takeOfferDate +
|
",\n takeOfferDate=" + takeOfferDate +
|
||||||
",\n processModel=" + processModel +
|
",\n processModel=" + processModel +
|
||||||
",\n payoutTxId='" + payoutTxId + '\'' +
|
",\n payoutTxId='" + payoutTxId + '\'' +
|
||||||
|
|
|
@ -353,16 +353,16 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
}
|
}
|
||||||
|
|
||||||
public TradeProtocol getTradeProtocol(Trade trade) {
|
public TradeProtocol getTradeProtocol(Trade trade) {
|
||||||
String uid = trade.getUid();
|
synchronized (tradeProtocolByTradeId) {
|
||||||
if (tradeProtocolByTradeId.containsKey(uid)) {
|
return tradeProtocolByTradeId.get(trade.getUid());
|
||||||
return tradeProtocolByTradeId.get(uid);
|
}
|
||||||
} else {
|
|
||||||
TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade);
|
|
||||||
TradeProtocol prev = tradeProtocolByTradeId.put(uid, tradeProtocol);
|
|
||||||
if (prev != null) {
|
|
||||||
log.error("We had already an entry with uid {}", trade.getUid());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TradeProtocol createTradeProtocol(Trade trade) {
|
||||||
|
synchronized (tradeProtocolByTradeId) {
|
||||||
|
TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade);
|
||||||
|
TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol);
|
||||||
|
if (prev != null) log.error("We had already an entry with uid {}", trade.getUid());
|
||||||
return tradeProtocol;
|
return tradeProtocol;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -377,6 +377,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
List<Trade> trades = getAllTrades();
|
List<Trade> trades = getAllTrades();
|
||||||
|
|
||||||
// open trades in parallel since each may open a multisig wallet
|
// open trades in parallel since each may open a multisig wallet
|
||||||
|
log.info("Initializing trades");
|
||||||
int threadPoolSize = 10;
|
int threadPoolSize = 10;
|
||||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
for (Trade trade : trades) {
|
for (Trade trade : trades) {
|
||||||
|
@ -387,8 +388,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
log.info("Initializing persisted trades");
|
|
||||||
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
||||||
|
log.info("Done initializing trades");
|
||||||
|
|
||||||
// reset any available address entries
|
// reset any available address entries
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
|
@ -419,7 +420,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
private void initPersistedTrade(Trade trade) {
|
private void initPersistedTrade(Trade trade) {
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
initTradeAndProtocol(trade, createTradeProtocol(trade));
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
scheduleDeletionIfUnfunded(trade);
|
scheduleDeletionIfUnfunded(trade);
|
||||||
}
|
}
|
||||||
|
@ -463,7 +464,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (offer == null) {
|
if (offer == null) {
|
||||||
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because no offer is on the books", sender, request.getTradeId());
|
log.warn("Ignoring InitTradeRequest from {} with tradeId {} because offer is not on the books", sender, request.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,7 +518,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash());
|
trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash());
|
||||||
}
|
}
|
||||||
|
|
||||||
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
// initialize trade protocol
|
||||||
|
initTradeAndProtocol(trade, createTradeProtocol(trade));
|
||||||
synchronized (tradableList) {
|
synchronized (tradableList) {
|
||||||
tradableList.add(trade);
|
tradableList.add(trade);
|
||||||
}
|
}
|
||||||
|
@ -596,7 +598,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
|
|
||||||
trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
||||||
trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing());
|
trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing());
|
||||||
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
initTradeAndProtocol(trade, createTradeProtocol(trade));
|
||||||
trade.getSelf().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId());
|
trade.getSelf().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId());
|
||||||
trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol?
|
trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol?
|
||||||
trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex());
|
trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex());
|
||||||
|
@ -782,11 +784,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
trade.getSelf().setPubKeyRing(model.getPubKeyRing());
|
trade.getSelf().setPubKeyRing(model.getPubKeyRing());
|
||||||
trade.getSelf().setPaymentAccountId(paymentAccountId);
|
trade.getSelf().setPaymentAccountId(paymentAccountId);
|
||||||
|
|
||||||
TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade);
|
// ensure trade is not already open
|
||||||
TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol);
|
Optional<Trade> tradeOptional = getOpenTrade(offer.getId());
|
||||||
if (prev != null) {
|
if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + trade.getId() + " is already open");
|
||||||
log.error("We had already an entry with uid {}", trade.getUid());
|
|
||||||
}
|
// initialize trade protocol
|
||||||
|
TradeProtocol tradeProtocol = createTradeProtocol(trade);
|
||||||
synchronized (tradableList) {
|
synchronized (tradableList) {
|
||||||
tradableList.add(trade);
|
tradableList.add(trade);
|
||||||
}
|
}
|
||||||
|
@ -804,11 +807,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
} else {
|
||||||
|
log.warn("Cannot take offer {} because it's not available, state={}", offer.getId(), offer.getState());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
errorMessage -> {
|
errorMessage -> {
|
||||||
log.warn("Taker error during check offer availability: " + errorMessage);
|
log.warn("Taker error during check offer availability: " + errorMessage);
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
@ -964,10 +970,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx
|
||||||
} else {
|
} else {
|
||||||
log.warn("We found a closed trade with locked up funds. " +
|
log.warn("We found a closed trade with locked up funds. " +
|
||||||
"That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
"That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -977,10 +983,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
|
||||||
} else {
|
} else {
|
||||||
log.warn("We found a closed trade with locked up funds. " +
|
log.warn("We found a closed trade with locked up funds. " +
|
||||||
"That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
"That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState());
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
||||||
}
|
}
|
||||||
return trade.getId();
|
return trade.getId();
|
||||||
|
|
|
@ -162,7 +162,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
|
|
||||||
// TODO (woodser): this method only necessary because isPubKeyValid not called with sender argument, so it's validated before
|
// TODO (woodser): this method only necessary because isPubKeyValid not called with sender argument, so it's validated before
|
||||||
private void handleMailboxCollectionSkipValidation(Collection<DecryptedMessageWithPubKey> collection) {
|
private void handleMailboxCollectionSkipValidation(Collection<DecryptedMessageWithPubKey> collection) {
|
||||||
log.warn("TradeProtocol.handleMailboxCollectionSkipValidation");
|
|
||||||
collection.stream()
|
collection.stream()
|
||||||
.map(DecryptedMessageWithPubKey::getNetworkEnvelope)
|
.map(DecryptedMessageWithPubKey::getNetworkEnvelope)
|
||||||
.filter(this::isMyMessage)
|
.filter(this::isMyMessage)
|
||||||
|
@ -817,6 +816,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||||
|
|
||||||
protected void latchTrade() {
|
protected void latchTrade() {
|
||||||
if (tradeLatch != null) throw new RuntimeException("Trade latch is not null. That should never happen.");
|
if (tradeLatch != null) throw new RuntimeException("Trade latch is not null. That should never happen.");
|
||||||
|
if (trade.isShutDown()) throw new RuntimeException("Cannot latch trade " + trade.getId() + " for protocol because it's shut down");
|
||||||
tradeLatch = new CountDownLatch(1);
|
tradeLatch = new CountDownLatch(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -85,6 +85,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||||
// verify deposit tx
|
// verify deposit tx
|
||||||
try {
|
try {
|
||||||
trade.getXmrWalletService().verifyTradeTx(
|
trade.getXmrWalletService().verifyTradeTx(
|
||||||
|
offer.getId(),
|
||||||
tradeFee,
|
tradeFee,
|
||||||
sendAmount,
|
sendAmount,
|
||||||
securityDeposit,
|
securityDeposit,
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class ArbitratorProcessReserveTx extends TradeTask {
|
||||||
Tuple2<MoneroTx, BigInteger> txResult;
|
Tuple2<MoneroTx, BigInteger> txResult;
|
||||||
try {
|
try {
|
||||||
txResult = trade.getXmrWalletService().verifyTradeTx(
|
txResult = trade.getXmrWalletService().verifyTradeTx(
|
||||||
|
offer.getId(),
|
||||||
tradeFee,
|
tradeFee,
|
||||||
sendAmount,
|
sendAmount,
|
||||||
securityDeposit,
|
securityDeposit,
|
||||||
|
|
|
@ -62,6 +62,9 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||||
// create payout tx if we have seller's updated multisig hex
|
// create payout tx if we have seller's updated multisig hex
|
||||||
if (trade.getSeller().getUpdatedMultisigHex() != null) {
|
if (trade.getSeller().getUpdatedMultisigHex() != null) {
|
||||||
|
|
||||||
|
// import multisig hex
|
||||||
|
trade.importMultisigHex();
|
||||||
|
|
||||||
// create payout tx
|
// create payout tx
|
||||||
log.info("Buyer creating unsigned payout tx");
|
log.info("Buyer creating unsigned payout tx");
|
||||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||||
|
|
|
@ -70,7 +70,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
|
||||||
trade.getSelf().getReserveTxHash(),
|
trade.getSelf().getReserveTxHash(),
|
||||||
trade.getSelf().getReserveTxHex(),
|
trade.getSelf().getReserveTxHex(),
|
||||||
trade.getSelf().getReserveTxKey(),
|
trade.getSelf().getReserveTxKey(),
|
||||||
model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(),
|
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
||||||
null);
|
null);
|
||||||
|
|
||||||
// send request to arbitrator
|
// send request to arbitrator
|
||||||
|
|
|
@ -55,7 +55,6 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
|
||||||
|
|
||||||
// update multisig hex
|
// update multisig hex
|
||||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||||
trade.importMultisigHex();
|
|
||||||
|
|
||||||
// decrypt seller payment account payload if key given
|
// decrypt seller payment account payload if key given
|
||||||
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
|
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
|
||||||
|
|
|
@ -126,6 +126,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||||
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
if (trade.getProcessModel().getPaymentSentMessage() == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId());
|
||||||
if (StringUtils.equals(trade.getPayoutTxHex(), trade.getProcessModel().getPaymentSentMessage().getPayoutTxHex())) { // unsigned
|
if (StringUtils.equals(trade.getPayoutTxHex(), trade.getProcessModel().getPaymentSentMessage().getPayoutTxHex())) { // unsigned
|
||||||
log.info("{} {} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName(), trade.getId());
|
log.info("{} {} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName(), trade.getId());
|
||||||
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
||||||
|
|
|
@ -44,22 +44,17 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
||||||
// verify signature of payment sent message
|
// verify signature of payment sent message
|
||||||
HavenoUtils.verifyPaymentSentMessage(trade, message);
|
HavenoUtils.verifyPaymentSentMessage(trade, message);
|
||||||
|
|
||||||
// set state
|
// update latest peer address
|
||||||
processModel.setPaymentSentMessage(message);
|
trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress());
|
||||||
trade.setPayoutTxHex(message.getPayoutTxHex());
|
|
||||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
|
||||||
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
|
|
||||||
|
|
||||||
// import multisig hex
|
|
||||||
trade.importMultisigHex();
|
|
||||||
|
|
||||||
// if seller, decrypt buyer's payment account payload
|
// if seller, decrypt buyer's payment account payload
|
||||||
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||||
|
|
||||||
// update latest peer address
|
// update state
|
||||||
trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress());
|
processModel.setPaymentSentMessage(message);
|
||||||
|
trade.setPayoutTxHex(message.getPayoutTxHex());
|
||||||
// set state
|
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
|
trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness());
|
||||||
String counterCurrencyTxId = message.getCounterCurrencyTxId();
|
String counterCurrencyTxId = message.getCounterCurrencyTxId();
|
||||||
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
||||||
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class TakerReserveTradeFunds extends TradeTask {
|
||||||
BigInteger takerFee = trade.getTakerFee();
|
BigInteger takerFee = trade.getTakerFee();
|
||||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
|
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
|
||||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
|
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
String returnAddress = model.getXmrWalletService().getAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
||||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress);
|
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress);
|
||||||
|
|
||||||
// collect reserved key images
|
// collect reserved key images
|
||||||
|
|
|
@ -93,7 +93,7 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted
|
||||||
return ImmutableList.copyOf(entrySet);
|
return ImmutableList.copyOf(entrySet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addAddressEntry(XmrAddressEntry addressEntry) {
|
public boolean addAddressEntry(XmrAddressEntry addressEntry) {
|
||||||
boolean entryWithSameOfferIdAndContextAlreadyExist = entrySet.stream().anyMatch(e -> {
|
boolean entryWithSameOfferIdAndContextAlreadyExist = entrySet.stream().anyMatch(e -> {
|
||||||
if (addressEntry.getOfferId() != null) {
|
if (addressEntry.getOfferId() != null) {
|
||||||
return addressEntry.getOfferId().equals(e.getOfferId()) && addressEntry.getContext() == e.getContext();
|
return addressEntry.getOfferId().equals(e.getOfferId()) && addressEntry.getContext() == e.getContext();
|
||||||
|
@ -101,14 +101,12 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
if (entryWithSameOfferIdAndContextAlreadyExist) {
|
if (entryWithSameOfferIdAndContextAlreadyExist) {
|
||||||
log.error("We have an address entry with the same offer ID and context. We do not add the new one. " +
|
throw new IllegalArgumentException("We have an address entry with the same offer ID and context. We do not add the new one. addressEntry=" + addressEntry);
|
||||||
"addressEntry={}, entrySet={}", addressEntry, entrySet);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean setChangedByAdd = entrySet.add(addressEntry);
|
boolean setChangedByAdd = entrySet.add(addressEntry);
|
||||||
if (setChangedByAdd)
|
if (setChangedByAdd) requestPersistence();
|
||||||
requestPersistence();
|
return setChangedByAdd;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void swapToAvailable(XmrAddressEntry addressEntry) {
|
public void swapToAvailable(XmrAddressEntry addressEntry) {
|
||||||
|
@ -123,9 +121,19 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted
|
||||||
public XmrAddressEntry swapAvailableToAddressEntryWithOfferId(XmrAddressEntry addressEntry,
|
public XmrAddressEntry swapAvailableToAddressEntryWithOfferId(XmrAddressEntry addressEntry,
|
||||||
XmrAddressEntry.Context context,
|
XmrAddressEntry.Context context,
|
||||||
String offerId) {
|
String offerId) {
|
||||||
|
// remove old entry
|
||||||
boolean setChangedByRemove = entrySet.remove(addressEntry);
|
boolean setChangedByRemove = entrySet.remove(addressEntry);
|
||||||
|
|
||||||
|
// add new entry
|
||||||
final XmrAddressEntry newAddressEntry = new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), context, offerId, null);
|
final XmrAddressEntry newAddressEntry = new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), context, offerId, null);
|
||||||
boolean setChangedByAdd = entrySet.add(newAddressEntry);
|
boolean setChangedByAdd = false;
|
||||||
|
try {
|
||||||
|
setChangedByAdd = addAddressEntry(newAddressEntry);
|
||||||
|
} catch (Exception e) {
|
||||||
|
entrySet.add(addressEntry); // undo change if error
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
if (setChangedByRemove || setChangedByAdd)
|
if (setChangedByRemove || setChangedByAdd)
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,7 @@ public class MoneroWalletRpcManager {
|
||||||
|
|
||||||
// stop process
|
// stop process
|
||||||
String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid());
|
String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid());
|
||||||
log.info("Stopping MoneroWalletRpc port: {} pid: {}", port, pid);
|
log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", walletRpc.getPath(), port, pid);
|
||||||
walletRpc.stopProcess();
|
walletRpc.stopProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -190,7 +190,9 @@ public class MoneroKeyImagePoller {
|
||||||
Set<String> containedKeyImages = new HashSet<String>(keyImages);
|
Set<String> containedKeyImages = new HashSet<String>(keyImages);
|
||||||
containedKeyImages.retainAll(this.keyImages);
|
containedKeyImages.retainAll(this.keyImages);
|
||||||
this.keyImages.removeAll(containedKeyImages);
|
this.keyImages.removeAll(containedKeyImages);
|
||||||
|
synchronized (lastStatuses) {
|
||||||
for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage);
|
for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage);
|
||||||
|
}
|
||||||
refreshPolling();
|
refreshPolling();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,12 +204,13 @@ public class MoneroKeyImagePoller {
|
||||||
* @return true if the key is spent, false if unspent, null if unknown
|
* @return true if the key is spent, false if unspent, null if unknown
|
||||||
*/
|
*/
|
||||||
public Boolean isSpent(String keyImage) {
|
public Boolean isSpent(String keyImage) {
|
||||||
|
synchronized (lastStatuses) {
|
||||||
if (!lastStatuses.containsKey(keyImage)) return null;
|
if (!lastStatuses.containsKey(keyImage)) return null;
|
||||||
return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT;
|
return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void poll() {
|
public void poll() {
|
||||||
synchronized (keyImages) {
|
|
||||||
if (daemon == null) {
|
if (daemon == null) {
|
||||||
log.warn("Cannot poll key images because daemon is null");
|
log.warn("Cannot poll key images because daemon is null");
|
||||||
return;
|
return;
|
||||||
|
@ -219,12 +222,16 @@ public class MoneroKeyImagePoller {
|
||||||
|
|
||||||
// collect changed statuses
|
// collect changed statuses
|
||||||
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
|
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
|
||||||
|
synchronized (lastStatuses) {
|
||||||
|
synchronized (keyImages) {
|
||||||
for (int i = 0; i < keyImages.size(); i++) {
|
for (int i = 0; i < keyImages.size(); i++) {
|
||||||
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
|
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
|
||||||
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
||||||
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// announce changes
|
// announce changes
|
||||||
if (!changedStatuses.isEmpty()) {
|
if (!changedStatuses.isEmpty()) {
|
||||||
|
@ -236,7 +243,6 @@ public class MoneroKeyImagePoller {
|
||||||
log.warn("Error polling key images: " + e.getMessage());
|
log.warn("Error polling key images: " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void refreshPolling() {
|
private void refreshPolling() {
|
||||||
setIsPolling(keyImages.size() > 0 && listeners.size() > 0);
|
setIsPolling(keyImages.size() > 0 && listeners.size() > 0);
|
||||||
|
@ -245,7 +251,7 @@ public class MoneroKeyImagePoller {
|
||||||
private synchronized void setIsPolling(boolean enabled) {
|
private synchronized void setIsPolling(boolean enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (!isPolling) {
|
if (!isPolling) {
|
||||||
isPolling = true; // TODO monero-java: looper.isPolling()
|
isPolling = true; // TODO: use looper.isStarted(), synchronize
|
||||||
looper.start(refreshPeriodMs);
|
looper.start(refreshPeriodMs);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -300,11 +300,14 @@ public class XmrWalletService {
|
||||||
* @return a transaction to reserve a trade
|
* @return a transaction to reserve a trade
|
||||||
*/
|
*/
|
||||||
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress) {
|
public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress) {
|
||||||
log.info("Creating reserve tx with fee={}, sendAmount={}, securityDeposit={}", tradeFee, sendAmount, securityDeposit);
|
log.info("Creating reserve tx with return address={}", returnAddress);
|
||||||
return createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true);
|
long time = System.currentTimeMillis();
|
||||||
|
MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true);
|
||||||
|
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
|
||||||
|
return reserveTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**s
|
||||||
* Create the multisig deposit tx and freeze its inputs.
|
* Create the multisig deposit tx and freeze its inputs.
|
||||||
*
|
*
|
||||||
* @param trade the trade to create a deposit tx from
|
* @param trade the trade to create a deposit tx from
|
||||||
|
@ -326,8 +329,11 @@ public class XmrWalletService {
|
||||||
thawOutputs(trade.getSelf().getReserveTxKeyImages());
|
thawOutputs(trade.getSelf().getReserveTxKeyImages());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Creating deposit tx with fee={}, sendAmount={}, securityDeposit={}", tradeFee, sendAmount, securityDeposit);
|
log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getId(), multisigAddress);
|
||||||
return createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false);
|
long time = System.currentTimeMillis();
|
||||||
|
MoneroTxWallet tradeTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false);
|
||||||
|
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time);
|
||||||
|
return tradeTx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,7 +384,7 @@ public class XmrWalletService {
|
||||||
* @param keyImages expected key images of inputs, ignored if null
|
* @param keyImages expected key images of inputs, ignored if null
|
||||||
* @return tuple with the verified tx and its actual security deposit
|
* @return tuple with the verified tx and its actual security deposit
|
||||||
*/
|
*/
|
||||||
public Tuple2<MoneroTx, BigInteger> verifyTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List<String> keyImages, boolean isReserveTx) {
|
public Tuple2<MoneroTx, BigInteger> verifyTradeTx(String offerId, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List<String> keyImages, boolean isReserveTx) {
|
||||||
MoneroDaemonRpc daemon = getDaemon();
|
MoneroDaemonRpc daemon = getDaemon();
|
||||||
MoneroWallet wallet = getWallet();
|
MoneroWallet wallet = getWallet();
|
||||||
MoneroTx tx = null;
|
MoneroTx tx = null;
|
||||||
|
@ -393,7 +399,10 @@ public class XmrWalletService {
|
||||||
// submit tx to pool
|
// submit tx to pool
|
||||||
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
|
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
|
||||||
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
|
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
|
||||||
tx = getTx(txHash);
|
|
||||||
|
// get pool tx which has weight and size
|
||||||
|
for (MoneroTx poolTx : daemon.getTxPool()) if (poolTx.getHash().equals(txHash)) tx = poolTx;
|
||||||
|
if (tx == null) throw new RuntimeException("Tx is not in pool after being submitted");
|
||||||
|
|
||||||
// verify key images
|
// verify key images
|
||||||
if (keyImages != null) {
|
if (keyImages != null) {
|
||||||
|
@ -426,7 +435,9 @@ public class XmrWalletService {
|
||||||
BigInteger actualSendAmount = returnCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit);
|
BigInteger actualSendAmount = returnCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit);
|
||||||
|
|
||||||
// verify trade fee
|
// verify trade fee
|
||||||
if (!tradeFee.equals(actualTradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + actualTradeFee);
|
if (!tradeFee.equals(actualTradeFee)) {
|
||||||
|
throw new RuntimeException("Trade fee is incorrect amount, expected=" + tradeFee + ", actual=" + actualTradeFee + ", return address check=" + JsonUtils.serialize(returnCheck) + ", fee address check=" + JsonUtils.serialize(feeCheck));
|
||||||
|
}
|
||||||
|
|
||||||
// verify sufficient security deposit
|
// verify sufficient security deposit
|
||||||
BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger();
|
BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger();
|
||||||
|
@ -436,6 +447,9 @@ public class XmrWalletService {
|
||||||
BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger());
|
BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger());
|
||||||
BigInteger actualDepositAndFee = actualSendAmount.add(actualSecurityDeposit).add(tx.getFee());
|
BigInteger actualDepositAndFee = actualSendAmount.add(actualSecurityDeposit).add(tx.getFee());
|
||||||
if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee);
|
if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage());
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
daemon.flushTxPool(txHash); // flush tx from pool
|
daemon.flushTxPool(txHash); // flush tx from pool
|
||||||
|
@ -524,7 +538,7 @@ public class XmrWalletService {
|
||||||
wallet = null;
|
wallet = null;
|
||||||
walletListeners.clear();
|
walletListeners.clear();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
log.warn("Error closing main monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,10 +560,10 @@ public class XmrWalletService {
|
||||||
|
|
||||||
private void maybeInitMainWallet() {
|
private void maybeInitMainWallet() {
|
||||||
if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
|
if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
|
||||||
MoneroDaemonRpc daemon = connectionsService.getDaemon();
|
|
||||||
log.info("Initializing main wallet with " + (daemon == null ? "daemon: null" : "monerod uri=" + daemon.getRpcConnection().getUri() + ", height=" + connectionsService.getLastInfo().getHeight()));
|
|
||||||
|
|
||||||
// open or create wallet
|
// open or create wallet
|
||||||
|
MoneroDaemonRpc daemon = connectionsService.getDaemon();
|
||||||
|
log.info("Initializing main wallet with " + (daemon == null ? "daemon: null" : "monerod uri=" + daemon.getRpcConnection().getUri()));
|
||||||
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
||||||
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
|
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
|
||||||
wallet = openWalletRpc(walletConfig, rpcBindPort);
|
wallet = openWalletRpc(walletConfig, rpcBindPort);
|
||||||
|
@ -593,7 +607,6 @@ public class XmrWalletService {
|
||||||
// must be connected to daemon
|
// must be connected to daemon
|
||||||
MoneroRpcConnection connection = connectionsService.getConnection();
|
MoneroRpcConnection connection = connectionsService.getConnection();
|
||||||
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
|
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
|
||||||
config.setServer(connection);
|
|
||||||
|
|
||||||
// start monero-wallet-rpc instance
|
// start monero-wallet-rpc instance
|
||||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||||
|
@ -607,7 +620,7 @@ public class XmrWalletService {
|
||||||
// create wallet
|
// create wallet
|
||||||
log.info("Creating wallet " + config.getPath() + " connected to daemon " + connection.getUri());
|
log.info("Creating wallet " + config.getPath() + " connected to daemon " + connection.getUri());
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
walletRpc.createWallet(config);
|
walletRpc.createWallet(config.setServer(connection));
|
||||||
log.info("Done creating wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
|
log.info("Done creating wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
|
||||||
return walletRpc;
|
return walletRpc;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -689,7 +702,13 @@ public class XmrWalletService {
|
||||||
wallet.setDaemonConnection(connection);
|
wallet.setDaemonConnection(connection);
|
||||||
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
||||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
||||||
new Thread(() -> wallet.sync()).start();
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
wallet.sync();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to sync main wallet after setting daemon connection: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -738,59 +757,57 @@ public class XmrWalletService {
|
||||||
|
|
||||||
// ----------------------------- LEGACY APP -------------------------------
|
// ----------------------------- LEGACY APP -------------------------------
|
||||||
|
|
||||||
public XmrAddressEntry getNewAddressEntry() {
|
public synchronized XmrAddressEntry getNewAddressEntry() {
|
||||||
return getOrCreateAddressEntry(XmrAddressEntry.Context.AVAILABLE, Optional.empty());
|
return getNewAddressEntry(XmrAddressEntry.Context.AVAILABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmrAddressEntry getFreshAddressEntry() {
|
public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||||
List<XmrAddressEntry> unusedAddressEntries = getUnusedAddressEntries();
|
|
||||||
if (unusedAddressEntries.isEmpty()) return getNewAddressEntry();
|
|
||||||
else return unusedAddressEntries.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) {
|
// try to use available and not yet used entries
|
||||||
var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE);
|
List<MoneroTxWallet> incomingTxs = getIncomingTxs(null); // pre-fetch all incoming txs to avoid query per subaddress
|
||||||
if (!available.isPresent()) return null;
|
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny();
|
||||||
return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
|
if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
||||||
}
|
|
||||||
|
|
||||||
public XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
// create new subaddress and entry
|
||||||
MoneroSubaddress subaddress = wallet.createSubaddress(0);
|
MoneroSubaddress subaddress = wallet.createSubaddress(0);
|
||||||
XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null);
|
XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null);
|
||||||
xmrAddressEntryList.addAddressEntry(entry);
|
xmrAddressEntryList.addAddressEntry(entry);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
public synchronized XmrAddressEntry getFreshAddressEntry() {
|
||||||
Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
List<XmrAddressEntry> unusedAddressEntries = getUnusedAddressEntries();
|
||||||
if (addressEntry.isPresent()) {
|
if (unusedAddressEntries.isEmpty()) return getNewAddressEntry();
|
||||||
return addressEntry.get();
|
else return unusedAddressEntries.get(0);
|
||||||
} else {
|
|
||||||
// We try to use available and not yet used entries
|
|
||||||
List<MoneroTxWallet> incomingTxs = getIncomingTxs(null); // pre-fetch all incoming txs to avoid query per subaddress
|
|
||||||
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext())
|
|
||||||
.filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny();
|
|
||||||
if (emptyAvailableAddressEntry.isPresent()) {
|
|
||||||
return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
|
||||||
} else {
|
|
||||||
return getNewAddressEntry(offerId, context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmrAddressEntry getArbitratorAddressEntry() {
|
public synchronized XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) {
|
||||||
|
var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE);
|
||||||
|
if (!available.isPresent()) return null;
|
||||||
|
return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||||
|
Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
||||||
|
if (addressEntry.isPresent()) return addressEntry.get();
|
||||||
|
else return getNewAddressEntry(offerId, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized XmrAddressEntry getArbitratorAddressEntry() {
|
||||||
XmrAddressEntry.Context context = XmrAddressEntry.Context.ARBITRATOR;
|
XmrAddressEntry.Context context = XmrAddressEntry.Context.ARBITRATOR;
|
||||||
Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
|
Optional<XmrAddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
|
||||||
.filter(e -> context == e.getContext())
|
.filter(e -> context == e.getContext())
|
||||||
.findAny();
|
.findAny();
|
||||||
return getOrCreateAddressEntry(context, addressEntry);
|
return addressEntry.isPresent() ? addressEntry.get() : getNewAddressEntry(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<XmrAddressEntry> getAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
public synchronized Optional<XmrAddressEntry> getAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||||
return getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
List<XmrAddressEntry> entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).collect(Collectors.toList());
|
||||||
|
if (entries.size() > 1) throw new RuntimeException("Multiple address entries exist with offer ID " + offerId + " and context " + context + ". That should never happen.");
|
||||||
|
return entries.isEmpty() ? Optional.empty() : Optional.of(entries.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) {
|
public synchronized void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) {
|
||||||
Optional<XmrAddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
Optional<XmrAddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny();
|
||||||
addressEntryOptional.ifPresent(e -> {
|
addressEntryOptional.ifPresent(e -> {
|
||||||
log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context);
|
log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context);
|
||||||
|
@ -799,13 +816,14 @@ public class XmrWalletService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetAddressEntriesForOpenOffer(String offerId) {
|
public synchronized void resetAddressEntriesForOpenOffer(String offerId) {
|
||||||
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
|
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE);
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE);
|
||||||
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetAddressEntriesForPendingTrade(String offerId) {
|
public synchronized void resetAddressEntriesForPendingTrade(String offerId) {
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.MULTI_SIG);
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.MULTI_SIG);
|
||||||
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases
|
// We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases
|
||||||
// where a user cannot send the funds
|
// where a user cannot send the funds
|
||||||
|
@ -821,18 +839,13 @@ public class XmrWalletService {
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
private XmrAddressEntry getOrCreateAddressEntry(XmrAddressEntry.Context context,
|
private XmrAddressEntry getNewAddressEntry(XmrAddressEntry.Context context) {
|
||||||
Optional<XmrAddressEntry> addressEntry) {
|
|
||||||
if (addressEntry.isPresent()) {
|
|
||||||
return addressEntry.get();
|
|
||||||
} else {
|
|
||||||
MoneroSubaddress subaddress = wallet.createSubaddress(0);
|
MoneroSubaddress subaddress = wallet.createSubaddress(0);
|
||||||
XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, null, null);
|
XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, null, null);
|
||||||
log.info("getOrCreateAddressEntry: add new XmrAddressEntry {}", entry);
|
log.info("getOrCreateAddressEntry: add new XmrAddressEntry {}", entry);
|
||||||
xmrAddressEntryList.addAddressEntry(entry);
|
xmrAddressEntryList.addAddressEntry(entry);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) {
|
private Optional<XmrAddressEntry> findAddressEntry(String address, XmrAddressEntry.Context context) {
|
||||||
return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny();
|
return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny();
|
||||||
|
|
|
@ -139,7 +139,7 @@ public class GrpcDisputesService extends DisputesImplBase {
|
||||||
new HashMap<>() {{
|
new HashMap<>() {{
|
||||||
put(getGetDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
put(getGetDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||||
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -20,6 +20,7 @@ package haveno.desktop.components;
|
||||||
import com.jfoenix.controls.JFXTextField;
|
import com.jfoenix.controls.JFXTextField;
|
||||||
import de.jensd.fx.fontawesome.AwesomeDude;
|
import de.jensd.fx.fontawesome.AwesomeDude;
|
||||||
import de.jensd.fx.fontawesome.AwesomeIcon;
|
import de.jensd.fx.fontawesome.AwesomeIcon;
|
||||||
|
import haveno.common.UserThread;
|
||||||
import haveno.common.util.Utilities;
|
import haveno.common.util.Utilities;
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.user.BlockChainExplorer;
|
import haveno.core.user.BlockChainExplorer;
|
||||||
|
@ -135,12 +136,14 @@ public class TxIdTextField extends AnchorPane {
|
||||||
};
|
};
|
||||||
xmrWalletService.addWalletListener(txUpdater);
|
xmrWalletService.addWalletListener(txUpdater);
|
||||||
|
|
||||||
updateConfidence(txId, true, null);
|
|
||||||
|
|
||||||
textField.setText(txId);
|
textField.setText(txId);
|
||||||
textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId));
|
textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId));
|
||||||
blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId));
|
blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId));
|
||||||
copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId));
|
copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId));
|
||||||
|
txConfidenceIndicator.setVisible(true);
|
||||||
|
|
||||||
|
// update off main thread
|
||||||
|
new Thread(() -> updateConfidence(txId, true, null)).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
|
@ -165,7 +168,7 @@ public class TxIdTextField extends AnchorPane {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateConfidence(String txId, boolean useCache, Long height) {
|
private synchronized void updateConfidence(String txId, boolean useCache, Long height) {
|
||||||
MoneroTx tx = null;
|
MoneroTx tx = null;
|
||||||
try {
|
try {
|
||||||
tx = useCache ? xmrWalletService.getTxWithCache(txId) : xmrWalletService.getTx(txId);
|
tx = useCache ? xmrWalletService.getTxWithCache(txId) : xmrWalletService.getTx(txId);
|
||||||
|
@ -173,14 +176,19 @@ public class TxIdTextField extends AnchorPane {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
|
updateConfidence(tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateConfidence(MoneroTx tx) {
|
||||||
|
UserThread.execute(() -> {
|
||||||
GUIUtil.updateConfidence(tx, progressIndicatorTooltip, txConfidenceIndicator);
|
GUIUtil.updateConfidence(tx, progressIndicatorTooltip, txConfidenceIndicator);
|
||||||
if (txConfidenceIndicator.getProgress() != 0) {
|
if (txConfidenceIndicator.getProgress() != 0) {
|
||||||
txConfidenceIndicator.setVisible(true);
|
|
||||||
AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0);
|
AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0);
|
||||||
}
|
}
|
||||||
if (txConfidenceIndicator.getProgress() >= 1.0 && txUpdater != null) {
|
if (txConfidenceIndicator.getProgress() >= 1.0 && txUpdater != null) {
|
||||||
xmrWalletService.removeWalletListener(txUpdater); // unregister listener
|
xmrWalletService.removeWalletListener(txUpdater); // unregister listener
|
||||||
txUpdater = null;
|
txUpdater = null;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
|
|
||||||
package haveno.desktop.components.indicator;
|
package haveno.desktop.components.indicator;
|
||||||
|
|
||||||
|
import haveno.common.UserThread;
|
||||||
import haveno.desktop.components.indicator.skin.StaticProgressIndicatorSkin;
|
import haveno.desktop.components.indicator.skin.StaticProgressIndicatorSkin;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.DoublePropertyBase;
|
import javafx.beans.property.DoublePropertyBase;
|
||||||
|
@ -220,7 +221,7 @@ public class TxConfidenceIndicator extends Control {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public final void setProgress(double value) {
|
public final void setProgress(double value) {
|
||||||
progressProperty().set(value);
|
UserThread.execute(() -> progressProperty().set(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public final DoubleProperty progressProperty() {
|
public final DoubleProperty progressProperty() {
|
||||||
|
|
|
@ -373,6 +373,7 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Double> minMaxFilterLeft(List<XYChart.Data<Number, Number>> data) {
|
private List<Double> minMaxFilterLeft(List<XYChart.Data<Number, Number>> data) {
|
||||||
|
synchronized (data) {
|
||||||
double maxValue = data.stream()
|
double maxValue = data.stream()
|
||||||
.mapToDouble(o -> o.getXValue().doubleValue())
|
.mapToDouble(o -> o.getXValue().doubleValue())
|
||||||
.max()
|
.max()
|
||||||
|
@ -385,8 +386,10 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
|
||||||
.orElse(Double.MAX_VALUE);
|
.orElse(Double.MAX_VALUE);
|
||||||
return List.of(minValue, maxValue);
|
return List.of(minValue, maxValue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<Double> minMaxFilterRight(List<XYChart.Data<Number, Number>> data) {
|
private List<Double> minMaxFilterRight(List<XYChart.Data<Number, Number>> data) {
|
||||||
|
synchronized (data) {
|
||||||
double minValue = data.stream()
|
double minValue = data.stream()
|
||||||
.mapToDouble(o -> o.getXValue().doubleValue())
|
.mapToDouble(o -> o.getXValue().doubleValue())
|
||||||
.min()
|
.min()
|
||||||
|
@ -400,18 +403,23 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
|
||||||
.orElse(Double.MIN_VALUE);
|
.orElse(Double.MIN_VALUE);
|
||||||
return List.of(minValue, maxValue);
|
return List.of(minValue, maxValue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<XYChart.Data<Number, Number>> filterLeft(List<XYChart.Data<Number, Number>> data, double maxValue) {
|
private List<XYChart.Data<Number, Number>> filterLeft(List<XYChart.Data<Number, Number>> data, double maxValue) {
|
||||||
|
synchronized (data) {
|
||||||
return data.stream()
|
return data.stream()
|
||||||
.filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor)
|
.filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<XYChart.Data<Number, Number>> filterRight(List<XYChart.Data<Number, Number>> data, double minValue) {
|
private List<XYChart.Data<Number, Number>> filterRight(List<XYChart.Data<Number, Number>> data, double minValue) {
|
||||||
|
synchronized (data) {
|
||||||
return data.stream()
|
return data.stream()
|
||||||
.filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor)
|
.filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Tuple4<TableView<OfferListItem>, VBox, Button, Label> getOfferTable(OfferDirection direction) {
|
private Tuple4<TableView<OfferListItem>, VBox, Button, Label> getOfferTable(OfferDirection direction) {
|
||||||
TableView<OfferListItem> tableView = new TableView<>();
|
TableView<OfferListItem> tableView = new TableView<>();
|
||||||
|
|
|
@ -389,6 +389,7 @@ class OfferBookChartViewModel extends ActivatableViewModel {
|
||||||
OfferDirection direction,
|
OfferDirection direction,
|
||||||
List<XYChart.Data<Number, Number>> data,
|
List<XYChart.Data<Number, Number>> data,
|
||||||
ObservableList<OfferListItem> offerTableList) {
|
ObservableList<OfferListItem> offerTableList) {
|
||||||
|
synchronized (data) {
|
||||||
data.clear();
|
data.clear();
|
||||||
double accumulatedAmount = 0;
|
double accumulatedAmount = 0;
|
||||||
List<OfferListItem> offerTableListTemp = new ArrayList<>();
|
List<OfferListItem> offerTableListTemp = new ArrayList<>();
|
||||||
|
@ -408,6 +409,7 @@ class OfferBookChartViewModel extends ActivatableViewModel {
|
||||||
}
|
}
|
||||||
offerTableList.setAll(offerTableListTemp);
|
offerTableList.setAll(offerTableListTemp);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isEditEntry(String id) {
|
private boolean isEditEntry(String id) {
|
||||||
return id.equals(GUIUtil.EDIT_FLAG);
|
return id.equals(GUIUtil.EDIT_FLAG);
|
||||||
|
|
|
@ -66,6 +66,8 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
@ -111,6 +113,8 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
trade = model.dataModel.getTrade();
|
trade = model.dataModel.getTrade();
|
||||||
checkNotNull(trade, "Trade must not be null at TradeStepView");
|
checkNotNull(trade, "Trade must not be null at TradeStepView");
|
||||||
|
|
||||||
|
startCachingTxs();
|
||||||
|
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||||
|
@ -166,16 +170,25 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
// };
|
// };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startCachingTxs() {
|
||||||
|
List<String> txIds = new ArrayList<String>();
|
||||||
|
if (!model.dataModel.makerTxId.isEmpty().get()) txIds.add(model.dataModel.makerTxId.get());
|
||||||
|
if (!model.dataModel.takerTxId.isEmpty().get()) txIds.add(model.dataModel.takerTxId.get());
|
||||||
|
new Thread(() -> trade.getXmrWalletService().getTxsWithCache(txIds)).start();
|
||||||
|
}
|
||||||
|
|
||||||
public void activate() {
|
public void activate() {
|
||||||
if (selfTxIdTextField != null) {
|
if (selfTxIdTextField != null) {
|
||||||
if (selfTxIdSubscription != null)
|
if (selfTxIdSubscription != null)
|
||||||
selfTxIdSubscription.unsubscribe();
|
selfTxIdSubscription.unsubscribe();
|
||||||
|
|
||||||
selfTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.makerTxId : model.dataModel.takerTxId, id -> {
|
selfTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.makerTxId : model.dataModel.takerTxId, id -> {
|
||||||
if (!id.isEmpty())
|
if (!id.isEmpty()) {
|
||||||
|
startCachingTxs();
|
||||||
selfTxIdTextField.setup(id);
|
selfTxIdTextField.setup(id);
|
||||||
else
|
} else {
|
||||||
selfTxIdTextField.cleanup();
|
selfTxIdTextField.cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (peerTxIdTextField != null) {
|
if (peerTxIdTextField != null) {
|
||||||
|
@ -183,10 +196,12 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
peerTxIdSubscription.unsubscribe();
|
peerTxIdSubscription.unsubscribe();
|
||||||
|
|
||||||
peerTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.takerTxId : model.dataModel.makerTxId, id -> {
|
peerTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.takerTxId : model.dataModel.makerTxId, id -> {
|
||||||
if (!id.isEmpty())
|
if (!id.isEmpty()) {
|
||||||
|
startCachingTxs();
|
||||||
peerTxIdTextField.setup(id);
|
peerTxIdTextField.setup(id);
|
||||||
else
|
} else {
|
||||||
peerTxIdTextField.cleanup();
|
peerTxIdTextField.cleanup();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
trade.errorMessageProperty().addListener(errorMessageListener);
|
trade.errorMessageProperty().addListener(errorMessageListener);
|
||||||
|
|
|
@ -223,7 +223,7 @@ public class BuyerStep2View extends TradeStepView {
|
||||||
addTradeInfoBlock();
|
addTradeInfoBlock();
|
||||||
|
|
||||||
PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
|
PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
|
||||||
String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "";
|
String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "<missing payment account payload>";
|
||||||
TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4,
|
TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4,
|
||||||
Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)),
|
Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)),
|
||||||
Layout.COMPACT_GROUP_DISTANCE);
|
Layout.COMPACT_GROUP_DISTANCE);
|
||||||
|
|
|
@ -1477,28 +1477,29 @@ message Trade {
|
||||||
string payout_tx_key = 5;
|
string payout_tx_key = 5;
|
||||||
int64 amount = 6;
|
int64 amount = 6;
|
||||||
int64 taker_fee = 8;
|
int64 taker_fee = 8;
|
||||||
int64 take_offer_date = 9;
|
int64 total_tx_fee = 9;
|
||||||
int64 price = 10;
|
int64 take_offer_date = 10;
|
||||||
State state = 11;
|
int64 price = 11;
|
||||||
PayoutState payout_state = 12;
|
State state = 12;
|
||||||
DisputeState dispute_state = 13;
|
PayoutState payout_state = 13;
|
||||||
TradePeriodState period_state = 14;
|
DisputeState dispute_state = 14;
|
||||||
Contract contract = 15;
|
TradePeriodState period_state = 15;
|
||||||
string contract_as_json = 16;
|
Contract contract = 16;
|
||||||
bytes contract_hash = 17;
|
string contract_as_json = 17;
|
||||||
NodeAddress arbitrator_node_address = 18;
|
bytes contract_hash = 18;
|
||||||
NodeAddress mediator_node_address = 19;
|
NodeAddress arbitrator_node_address = 19;
|
||||||
string error_message = 20;
|
NodeAddress mediator_node_address = 20;
|
||||||
string counter_currency_tx_id = 21;
|
string error_message = 21;
|
||||||
repeated ChatMessage chat_message = 22;
|
string counter_currency_tx_id = 22;
|
||||||
MediationResultState mediation_result_state = 23;
|
repeated ChatMessage chat_message = 23;
|
||||||
int64 lock_time = 24;
|
MediationResultState mediation_result_state = 24;
|
||||||
int64 start_time = 25;
|
int64 lock_time = 25;
|
||||||
NodeAddress refund_agent_node_address = 26;
|
int64 start_time = 26;
|
||||||
RefundResultState refund_result_state = 27;
|
NodeAddress refund_agent_node_address = 27;
|
||||||
string counter_currency_extra_data = 28;
|
RefundResultState refund_result_state = 28;
|
||||||
string asset_tx_proof_result = 29; // name of AssetTxProofResult enum
|
string counter_currency_extra_data = 29;
|
||||||
string uid = 30;
|
string asset_tx_proof_result = 30; // name of AssetTxProofResult enum
|
||||||
|
string uid = 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BuyerAsMakerTrade {
|
message BuyerAsMakerTrade {
|
||||||
|
|
Loading…
Reference in a new issue