mirror of
https://github.com/retoaccess1/haveno-reto.git
synced 2024-12-11 12:23:32 +01:00
support multithreading in api and protocols
close trade wallets while unused for scalability verify txs do not use unlock height increase trade init timeout to 60s
This commit is contained in:
parent
fdddc87477
commit
bb95b4b1d6
9
Makefile
9
Makefile
@ -14,8 +14,13 @@ localnet:
|
|||||||
haveno:
|
haveno:
|
||||||
./gradlew build
|
./gradlew build
|
||||||
|
|
||||||
haveno-apps: # quick build desktop and daemon apps without tests, etc
|
# build haveno without tests
|
||||||
./gradlew :core:compileJava :desktop:build
|
no-tests:
|
||||||
|
./gradlew build -x test
|
||||||
|
|
||||||
|
# quick build desktop and daemon apps without tests
|
||||||
|
haveno-apps:
|
||||||
|
./gradlew :core:compileJava :desktop:build -x test
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
# create a new screen session named 'localnet'
|
# create a new screen session named 'localnet'
|
||||||
|
@ -42,43 +42,61 @@ public abstract class PersistableList<T extends PersistablePayload> implements P
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setAll(Collection<T> collection) {
|
public void setAll(Collection<T> collection) {
|
||||||
this.list.clear();
|
synchronized (this.list) {
|
||||||
this.list.addAll(collection);
|
this.list.clear();
|
||||||
|
this.list.addAll(collection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean add(T item) {
|
public boolean add(T item) {
|
||||||
if (!list.contains(item)) {
|
synchronized (list) {
|
||||||
list.add(item);
|
if (!list.contains(item)) {
|
||||||
return true;
|
list.add(item);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean remove(T item) {
|
public boolean remove(T item) {
|
||||||
return list.remove(item);
|
synchronized (list) {
|
||||||
|
return list.remove(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stream<T> stream() {
|
public Stream<T> stream() {
|
||||||
return list.stream();
|
synchronized (list) {
|
||||||
|
return list.stream();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int size() {
|
public int size() {
|
||||||
return list.size();
|
synchronized (list) {
|
||||||
|
return list.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean contains(T item) {
|
public boolean contains(T item) {
|
||||||
return list.contains(item);
|
synchronized (list) {
|
||||||
|
return list.contains(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return list.isEmpty();
|
synchronized (list) {
|
||||||
|
return list.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void forEach(Consumer<? super T> action) {
|
public void forEach(Consumer<? super T> action) {
|
||||||
list.forEach(action);
|
synchronized (list) {
|
||||||
|
list.forEach(action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
list.clear();
|
synchronized (list) {
|
||||||
|
list.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,11 +66,14 @@ public abstract class Task<T extends Model> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void failed(Throwable t) {
|
protected void failed(Throwable t) {
|
||||||
StringWriter sw = new StringWriter();
|
// // append stacktrace to error message (only for development)
|
||||||
PrintWriter pw = new PrintWriter(sw);
|
// StringWriter sw = new StringWriter();
|
||||||
t.printStackTrace(pw);
|
// PrintWriter pw = new PrintWriter(sw);
|
||||||
errorMessage = sw.toString();
|
// t.printStackTrace(pw);
|
||||||
log.error(t.getMessage(), t);
|
// errorMessage = sw.toString();
|
||||||
|
|
||||||
|
errorMessage = t.getMessage() + " (task " + getClass().getSimpleName() + ")";
|
||||||
|
log.error(errorMessage, t);
|
||||||
taskHandler.handleErrorMessage(errorMessage);
|
taskHandler.handleErrorMessage(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +434,8 @@ public class CoreApi {
|
|||||||
double buyerSecurityDeposit,
|
double buyerSecurityDeposit,
|
||||||
long triggerPrice,
|
long triggerPrice,
|
||||||
String paymentAccountId,
|
String paymentAccountId,
|
||||||
Consumer<Offer> resultHandler) {
|
Consumer<Offer> resultHandler,
|
||||||
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
coreOffersService.createAndPlaceOffer(currencyCode,
|
coreOffersService.createAndPlaceOffer(currencyCode,
|
||||||
directionAsString,
|
directionAsString,
|
||||||
priceAsString,
|
priceAsString,
|
||||||
@ -445,7 +446,8 @@ public class CoreApi {
|
|||||||
buyerSecurityDeposit,
|
buyerSecurityDeposit,
|
||||||
triggerPrice,
|
triggerPrice,
|
||||||
paymentAccountId,
|
paymentAccountId,
|
||||||
resultHandler);
|
resultHandler,
|
||||||
|
errorMessageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Offer editOffer(String offerId,
|
public Offer editOffer(String offerId,
|
||||||
|
@ -85,113 +85,122 @@ public class CoreDisputesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) {
|
public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) {
|
||||||
Trade trade = tradeManager.getTradeById(tradeId).orElseThrow(() ->
|
Trade trade = tradeManager.getOpenTrade(tradeId).orElseThrow(() ->
|
||||||
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
|
||||||
|
|
||||||
Offer offer = trade.getOffer();
|
synchronized (trade) {
|
||||||
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
|
Offer offer = trade.getOffer();
|
||||||
|
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
|
||||||
|
|
||||||
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
|
// Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded
|
||||||
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
|
// to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored.
|
||||||
var disputeManager = arbitrationManager;
|
var disputeManager = arbitrationManager;
|
||||||
var isSupportTicket = false;
|
var isSupportTicket = false;
|
||||||
var isMaker = tradeManager.isMyOffer(offer);
|
var isMaker = tradeManager.isMyOffer(offer);
|
||||||
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
|
var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket);
|
||||||
|
|
||||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
||||||
String updatedMultisigHex = multisigWallet.getMultisigHex();
|
String updatedMultisigHex = multisigWallet.getMultisigHex();
|
||||||
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
|
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
|
|
||||||
|
// close multisig wallet
|
||||||
|
xmrWalletService.closeMultisigWallet(trade.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
||||||
byte[] payoutTxSerialized = null;
|
synchronized (trade) {
|
||||||
String payoutTxHashAsString = null;
|
byte[] payoutTxSerialized = null;
|
||||||
|
String payoutTxHashAsString = null;
|
||||||
|
|
||||||
PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing();
|
PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing();
|
||||||
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
|
checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null");
|
||||||
byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser)
|
byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser)
|
||||||
String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser)
|
String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser)
|
||||||
Dispute dispute = new Dispute(new Date().getTime(),
|
Dispute dispute = new Dispute(new Date().getTime(),
|
||||||
trade.getId(),
|
trade.getId(),
|
||||||
pubKey.hashCode(), // trader id,
|
pubKey.hashCode(), // trader id,
|
||||||
true,
|
true,
|
||||||
(offer.getDirection() == OfferPayload.Direction.BUY) == isMaker,
|
(offer.getDirection() == OfferPayload.Direction.BUY) == isMaker,
|
||||||
isMaker,
|
isMaker,
|
||||||
pubKey,
|
pubKey,
|
||||||
trade.getDate().getTime(),
|
trade.getDate().getTime(),
|
||||||
trade.getMaxTradePeriodDate().getTime(),
|
trade.getMaxTradePeriodDate().getTime(),
|
||||||
trade.getContract(),
|
trade.getContract(),
|
||||||
trade.getContractHash(),
|
trade.getContractHash(),
|
||||||
depositTxSerialized,
|
depositTxSerialized,
|
||||||
payoutTxSerialized,
|
payoutTxSerialized,
|
||||||
depositTxHashAsString,
|
depositTxHashAsString,
|
||||||
payoutTxHashAsString,
|
payoutTxHashAsString,
|
||||||
trade.getContractAsJson(),
|
trade.getContractAsJson(),
|
||||||
trade.getMaker().getContractSignature(),
|
trade.getMaker().getContractSignature(),
|
||||||
trade.getTaker().getContractSignature(),
|
trade.getTaker().getContractSignature(),
|
||||||
trade.getMaker().getPaymentAccountPayload(),
|
trade.getMaker().getPaymentAccountPayload(),
|
||||||
trade.getTaker().getPaymentAccountPayload(),
|
trade.getTaker().getPaymentAccountPayload(),
|
||||||
arbitratorPubKeyRing,
|
arbitratorPubKeyRing,
|
||||||
isSupportTicket,
|
isSupportTicket,
|
||||||
SupportType.ARBITRATION);
|
SupportType.ARBITRATION);
|
||||||
|
|
||||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||||
|
|
||||||
return dispute;
|
return dispute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
|
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
|
||||||
try {
|
try {
|
||||||
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
|
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
|
||||||
.filter(d -> tradeId.equals(d.getTradeId()))
|
.filter(d -> tradeId.equals(d.getTradeId()))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
Dispute dispute;
|
Dispute dispute;
|
||||||
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
|
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
|
||||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||||
|
|
||||||
var closeDate = new Date();
|
synchronized (tradeManager.getTrade(tradeId)) {
|
||||||
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
|
var closeDate = new Date();
|
||||||
var contract = dispute.getContract();
|
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
|
||||||
|
var contract = dispute.getContract();
|
||||||
|
|
||||||
DisputePayout payout;
|
DisputePayout payout;
|
||||||
if (customWinnerAmount > 0) {
|
if (customWinnerAmount > 0) {
|
||||||
payout = DisputePayout.CUSTOM;
|
payout = DisputePayout.CUSTOM;
|
||||||
} else if (winner == DisputeResult.Winner.BUYER) {
|
} else if (winner == DisputeResult.Winner.BUYER) {
|
||||||
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
|
payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT;
|
||||||
} else if (winner == DisputeResult.Winner.SELLER) {
|
} else if (winner == DisputeResult.Winner.SELLER) {
|
||||||
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
|
payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT;
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner);
|
||||||
|
}
|
||||||
|
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
|
||||||
|
|
||||||
|
// resolve the payout
|
||||||
|
resolveDisputePayout(dispute, disputeResult, contract);
|
||||||
|
|
||||||
|
// close dispute ticket
|
||||||
|
closeDispute(arbitrationManager, dispute, disputeResult, false);
|
||||||
|
|
||||||
|
// close dispute ticket for peer
|
||||||
|
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
|
||||||
|
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
|
||||||
|
.findFirst();
|
||||||
|
|
||||||
|
if (peersDisputeOptional.isPresent()) {
|
||||||
|
var peerDispute = peersDisputeOptional.get();
|
||||||
|
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
|
||||||
|
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
|
||||||
|
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
|
||||||
|
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
|
||||||
|
resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
|
||||||
|
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("could not find peer dispute");
|
||||||
|
}
|
||||||
|
|
||||||
|
arbitrationManager.requestPersistence();
|
||||||
}
|
}
|
||||||
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
|
|
||||||
|
|
||||||
// resolve the payout
|
|
||||||
resolveDisputePayout(dispute, disputeResult, contract);
|
|
||||||
|
|
||||||
// close dispute ticket
|
|
||||||
closeDispute(arbitrationManager, dispute, disputeResult, false);
|
|
||||||
|
|
||||||
// close dispute ticket for peer
|
|
||||||
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
|
|
||||||
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
if (peersDisputeOptional.isPresent()) {
|
|
||||||
var peerDispute = peersDisputeOptional.get();
|
|
||||||
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
|
|
||||||
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
|
|
||||||
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
|
|
||||||
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
|
|
||||||
resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
|
|
||||||
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("could not find peer dispute");
|
|
||||||
}
|
|
||||||
|
|
||||||
arbitrationManager.requestPersistence();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
@ -245,29 +254,36 @@ public class CoreDisputesService {
|
|||||||
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
|
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
|
||||||
if (!dispute.isMediationDispute()) {
|
if (!dispute.isMediationDispute()) {
|
||||||
try {
|
try {
|
||||||
System.out.println(disputeResult);
|
synchronized (tradeManager.getTrade(dispute.getTradeId())) {
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
System.out.println(disputeResult);
|
||||||
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
|
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
|
||||||
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
|
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
|
||||||
|
|
||||||
// TODO (woodser): don't send signed tx if opener is not co-signer?
|
// TODO (woodser): don't send signed tx if opener is not co-signer?
|
||||||
// // determine if opener is co-signer
|
// // determine if opener is co-signer
|
||||||
// boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER);
|
// boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER);
|
||||||
// boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher();
|
// boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher();
|
||||||
// if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx");
|
// if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx");
|
||||||
|
|
||||||
// arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex
|
// open multisig wallet
|
||||||
boolean isOpener = dispute.isOpener();
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||||
System.out.println("Is dispute opener: " + isOpener);
|
|
||||||
if (isOpener) {
|
// arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex
|
||||||
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
boolean isOpener = dispute.isOpener();
|
||||||
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
|
System.out.println("Is dispute opener: " + isOpener);
|
||||||
if (arbitratorPayoutTx != null)
|
if (isOpener) {
|
||||||
disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
|
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
||||||
|
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
|
||||||
|
if (arbitratorPayoutTx != null)
|
||||||
|
disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
// send arbitrator's updated multisig hex with dispute result
|
||||||
|
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex());
|
||||||
|
|
||||||
|
// close multisig wallet
|
||||||
|
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// send arbitrator's updated multisig hex with dispute result
|
|
||||||
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex());
|
|
||||||
} catch (AddressFormatException e2) {
|
} catch (AddressFormatException e2) {
|
||||||
log.error("Error at close dispute", e2);
|
log.error("Error at close dispute", e2);
|
||||||
return;
|
return;
|
||||||
|
@ -32,7 +32,7 @@ import bisq.core.payment.PaymentAccount;
|
|||||||
import bisq.core.user.User;
|
import bisq.core.user.User;
|
||||||
|
|
||||||
import bisq.common.crypto.KeyRing;
|
import bisq.common.crypto.KeyRing;
|
||||||
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.bitcoinj.utils.Fiat;
|
import org.bitcoinj.utils.Fiat;
|
||||||
@ -175,7 +175,10 @@ class CoreOffersService {
|
|||||||
List<String> allKeyImages = new ArrayList<String>();
|
List<String> allKeyImages = new ArrayList<String>();
|
||||||
for (Offer offer : offers) {
|
for (Offer offer : offers) {
|
||||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||||
if (!allKeyImages.add(keyImage)) unreservedOffers.add(offer);
|
if (!allKeyImages.add(keyImage)) {
|
||||||
|
log.warn("Key image {} belongs to another offer, removing offer {}", keyImage, offer.getId());
|
||||||
|
unreservedOffers.add(offer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,7 +194,10 @@ class CoreOffersService {
|
|||||||
for (Offer offer : offers) {
|
for (Offer offer : offers) {
|
||||||
if (unreservedOffers.contains(offer)) continue;
|
if (unreservedOffers.contains(offer)) continue;
|
||||||
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
for (String keyImage : offer.getOfferPayload().getReserveTxKeyImages()) {
|
||||||
if (spentKeyImages.contains(keyImage)) unreservedOffers.add(offer);
|
if (spentKeyImages.contains(keyImage)) {
|
||||||
|
log.warn("Offer {} reserved funds have already been spent with key image {}", offer.getId(), keyImage);
|
||||||
|
unreservedOffers.add(offer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +222,8 @@ class CoreOffersService {
|
|||||||
double buyerSecurityDeposit,
|
double buyerSecurityDeposit,
|
||||||
long triggerPrice,
|
long triggerPrice,
|
||||||
String paymentAccountId,
|
String paymentAccountId,
|
||||||
Consumer<Offer> resultHandler) {
|
Consumer<Offer> resultHandler,
|
||||||
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
coreWalletsService.verifyWalletsAreAvailable();
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
|
|
||||||
@ -252,7 +259,8 @@ class CoreOffersService {
|
|||||||
buyerSecurityDeposit,
|
buyerSecurityDeposit,
|
||||||
triggerPrice,
|
triggerPrice,
|
||||||
useSavingsWallet,
|
useSavingsWallet,
|
||||||
transaction -> resultHandler.accept(offer));
|
transaction -> resultHandler.accept(offer),
|
||||||
|
errorMessageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit a placed offer.
|
// Edit a placed offer.
|
||||||
@ -303,16 +311,14 @@ class CoreOffersService {
|
|||||||
double buyerSecurityDeposit,
|
double buyerSecurityDeposit,
|
||||||
long triggerPrice,
|
long triggerPrice,
|
||||||
boolean useSavingsWallet,
|
boolean useSavingsWallet,
|
||||||
Consumer<Transaction> resultHandler) {
|
Consumer<Transaction> resultHandler,
|
||||||
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
openOfferManager.placeOffer(offer,
|
openOfferManager.placeOffer(offer,
|
||||||
buyerSecurityDeposit,
|
buyerSecurityDeposit,
|
||||||
useSavingsWallet,
|
useSavingsWallet,
|
||||||
triggerPrice,
|
triggerPrice,
|
||||||
resultHandler::accept,
|
resultHandler::accept,
|
||||||
log::error);
|
errorMessageHandler);
|
||||||
|
|
||||||
if (offer.getErrorMessage() != null)
|
|
||||||
throw new IllegalStateException(offer.getErrorMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean offerMatchesDirectionAndCurrency(Offer offer,
|
private boolean offerMatchesDirectionAndCurrency(Offer offer,
|
||||||
|
@ -100,7 +100,7 @@ class CorePaymentAccountsService {
|
|||||||
|
|
||||||
// Crypto Currency Accounts
|
// Crypto Currency Accounts
|
||||||
|
|
||||||
PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
|
synchronized PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
|
||||||
String currencyCode,
|
String currencyCode,
|
||||||
String address,
|
String address,
|
||||||
boolean tradeInstant) {
|
boolean tradeInstant) {
|
||||||
|
@ -103,16 +103,24 @@ class CoreTradesService {
|
|||||||
throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId));
|
throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId));
|
||||||
|
|
||||||
var useSavingsWallet = true;
|
var useSavingsWallet = true;
|
||||||
//noinspection ConstantConditions
|
|
||||||
takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet);
|
// synchronize access to take offer model // TODO (woodser): to avoid synchronizing, don't use stateful model
|
||||||
log.info("Initiating take {} offer, {}",
|
Coin txFeeFromFeeService; // TODO (woodser): remove this and other unused fields
|
||||||
offer.isBuyOffer() ? "buy" : "sell",
|
Coin takerFee;
|
||||||
takeOfferModel);
|
Coin fundsNeededForTrade;
|
||||||
//noinspection ConstantConditions
|
synchronized (takeOfferModel) {
|
||||||
|
takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet);
|
||||||
|
txFeeFromFeeService = takeOfferModel.getTxFeeFromFeeService();
|
||||||
|
takerFee = takeOfferModel.getTakerFee();
|
||||||
|
fundsNeededForTrade = takeOfferModel.getFundsNeededForTrade();
|
||||||
|
log.info("Initiating take {} offer, {}", offer.isBuyOffer() ? "buy" : "sell", takeOfferModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// take offer
|
||||||
tradeManager.onTakeOffer(offer.getAmount(),
|
tradeManager.onTakeOffer(offer.getAmount(),
|
||||||
takeOfferModel.getTxFeeFromFeeService(),
|
txFeeFromFeeService,
|
||||||
takeOfferModel.getTakerFee(),
|
takerFee,
|
||||||
takeOfferModel.getFundsNeededForTrade(),
|
fundsNeededForTrade,
|
||||||
offer,
|
offer,
|
||||||
paymentAccountId,
|
paymentAccountId,
|
||||||
useSavingsWallet,
|
useSavingsWallet,
|
||||||
@ -225,7 +233,7 @@ class CoreTradesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Trade> getOpenTrade(String tradeId) {
|
private Optional<Trade> getOpenTrade(String tradeId) {
|
||||||
return tradeManager.getTradeById(tradeId);
|
return tradeManager.getOpenTrade(tradeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Trade> getClosedTrade(String tradeId) {
|
private Optional<Trade> getClosedTrade(String tradeId) {
|
||||||
@ -236,14 +244,14 @@ class CoreTradesService {
|
|||||||
List<Trade> getTrades() {
|
List<Trade> getTrades() {
|
||||||
coreWalletsService.verifyWalletsAreAvailable();
|
coreWalletsService.verifyWalletsAreAvailable();
|
||||||
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
coreWalletsService.verifyEncryptedWalletIsUnlocked();
|
||||||
List<Trade> trades = new ArrayList<Trade>(tradeManager.getTrades());
|
List<Trade> trades = new ArrayList<Trade>(tradeManager.getOpenTrades());
|
||||||
trades.addAll(closedTradableManager.getClosedTrades());
|
trades.addAll(closedTradableManager.getClosedTrades());
|
||||||
return trades;
|
return trades;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ChatMessage> getChatMessages(String tradeId) {
|
List<ChatMessage> getChatMessages(String tradeId) {
|
||||||
Trade trade;
|
Trade trade;
|
||||||
var tradeOptional = tradeManager.getTradeById(tradeId);
|
var tradeOptional = tradeManager.getOpenTrade(tradeId);
|
||||||
if (tradeOptional.isPresent()) trade = tradeOptional.get();
|
if (tradeOptional.isPresent()) trade = tradeOptional.get();
|
||||||
else throw new IllegalStateException(format("trade with id '%s' not found", tradeId));
|
else throw new IllegalStateException(format("trade with id '%s' not found", tradeId));
|
||||||
boolean isMaker = tradeManager.isMyOffer(trade.getOffer());
|
boolean isMaker = tradeManager.isMyOffer(trade.getOffer());
|
||||||
@ -253,7 +261,7 @@ class CoreTradesService {
|
|||||||
|
|
||||||
void sendChatMessage(String tradeId, String message) {
|
void sendChatMessage(String tradeId, String message) {
|
||||||
Trade trade;
|
Trade trade;
|
||||||
var tradeOptional = tradeManager.getTradeById(tradeId);
|
var tradeOptional = tradeManager.getOpenTrade(tradeId);
|
||||||
if (tradeOptional.isPresent()) trade = tradeOptional.get();
|
if (tradeOptional.isPresent()) trade = tradeOptional.get();
|
||||||
else throw new IllegalStateException(format("trade with id '%s' not found", tradeId));
|
else throw new IllegalStateException(format("trade with id '%s' not found", tradeId));
|
||||||
boolean isMaker = tradeManager.isMyOffer(trade.getOffer());
|
boolean isMaker = tradeManager.isMyOffer(trade.getOffer());
|
||||||
|
@ -509,12 +509,12 @@ class CoreWalletsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Throws a RuntimeException if wallet currency code is not BTC.
|
// Throws a RuntimeException if wallet currency code is not BTC or XMR.
|
||||||
private void verifyWalletCurrencyCodeIsValid(String currencyCode) {
|
private void verifyWalletCurrencyCodeIsValid(String currencyCode) {
|
||||||
if (currencyCode == null || currencyCode.isEmpty())
|
if (currencyCode == null || currencyCode.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!currencyCode.equalsIgnoreCase("BTC"))
|
if (!currencyCode.equalsIgnoreCase("BTC") && !currencyCode.equalsIgnoreCase("XMR"))
|
||||||
throw new IllegalStateException(format("wallet does not support %s", currencyCode));
|
throw new IllegalStateException(format("wallet does not support %s", currencyCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ public class Balances {
|
|||||||
|
|
||||||
private void updatedBalances() {
|
private void updatedBalances() {
|
||||||
// Need to delay a bit to get the balances correct
|
// Need to delay a bit to get the balances correct
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> { // TODO (woodser): running on user thread because JFX properties updated for legacy app
|
||||||
updateAvailableBalance();
|
updateAvailableBalance();
|
||||||
updateLockedBalance();
|
updateLockedBalance();
|
||||||
updateReservedOfferBalance();
|
updateReservedOfferBalance();
|
||||||
|
@ -46,44 +46,51 @@ public class MoneroWalletRpcManager {
|
|||||||
* @return a client connected to the monero-wallet-rpc instance
|
* @return a client connected to the monero-wallet-rpc instance
|
||||||
*/
|
*/
|
||||||
public MoneroWalletRpc startInstance(List<String> cmd) {
|
public MoneroWalletRpc startInstance(List<String> cmd) {
|
||||||
|
try {
|
||||||
|
|
||||||
try {
|
// register given port
|
||||||
|
if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) {
|
||||||
// register given port
|
int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1;
|
||||||
if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) {
|
int port = Integer.parseInt(cmd.get(portArgumentPosition));
|
||||||
int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1;
|
synchronized (registeredPorts) {
|
||||||
int port = Integer.parseInt(cmd.get(portArgumentPosition));
|
if (registeredPorts.containsKey(port)) throw new RuntimeException("Port " + port + " is already registered");
|
||||||
MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process
|
registeredPorts.put(port, null);
|
||||||
registeredPorts.put(port, walletRpc);
|
}
|
||||||
return walletRpc;
|
MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process
|
||||||
}
|
synchronized (registeredPorts) {
|
||||||
|
registeredPorts.put(port, walletRpc);
|
||||||
// register assigned ports up to maximum attempts
|
}
|
||||||
else {
|
return walletRpc;
|
||||||
int numAttempts = 0;
|
|
||||||
while (numAttempts < NUM_ALLOWED_ATTEMPTS) {
|
|
||||||
int port = -1;
|
|
||||||
try {
|
|
||||||
numAttempts++;
|
|
||||||
port = registerPort();
|
|
||||||
List<String> cmdCopy = new ArrayList<>(cmd); // preserve original cmd
|
|
||||||
cmdCopy.add(RPC_BIND_PORT_ARGUMENT);
|
|
||||||
cmdCopy.add("" + port);
|
|
||||||
MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process
|
|
||||||
registeredPorts.put(port, walletRpc);
|
|
||||||
return walletRpc;
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (numAttempts >= NUM_ALLOWED_ATTEMPTS) {
|
|
||||||
log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here
|
// register assigned ports up to maximum attempts
|
||||||
|
else {
|
||||||
|
int numAttempts = 0;
|
||||||
|
while (numAttempts < NUM_ALLOWED_ATTEMPTS) {
|
||||||
|
int port = -1;
|
||||||
|
try {
|
||||||
|
numAttempts++;
|
||||||
|
port = registerPort();
|
||||||
|
List<String> cmdCopy = new ArrayList<>(cmd); // preserve original cmd
|
||||||
|
cmdCopy.add(RPC_BIND_PORT_ARGUMENT);
|
||||||
|
cmdCopy.add("" + port);
|
||||||
|
MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process
|
||||||
|
synchronized (registeredPorts) {
|
||||||
|
registeredPorts.put(port, walletRpc);
|
||||||
|
}
|
||||||
|
return walletRpc;
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (numAttempts >= NUM_ALLOWED_ATTEMPTS) {
|
||||||
|
log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new MoneroError(e);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
throw new MoneroError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,41 +100,59 @@ public class MoneroWalletRpcManager {
|
|||||||
* @param save specifies if the wallet should be saved before closing
|
* @param save specifies if the wallet should be saved before closing
|
||||||
*/
|
*/
|
||||||
public void stopInstance(MoneroWalletRpc walletRpc, boolean save) {
|
public void stopInstance(MoneroWalletRpc walletRpc, boolean save) {
|
||||||
boolean found = false;
|
|
||||||
for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) {
|
// unregister port
|
||||||
if (walletRpc == entry.getValue()) {
|
synchronized (registeredPorts) {
|
||||||
walletRpc.close(save);
|
boolean found = false;
|
||||||
walletRpc.stopProcess();
|
for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) {
|
||||||
found = true;
|
if (walletRpc == entry.getValue()) {
|
||||||
try { unregisterPort(entry.getKey()); }
|
found = true;
|
||||||
catch (Exception e) { throw new MoneroError(e); }
|
try {
|
||||||
break;
|
unregisterPort(entry.getKey());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MoneroError(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!found) throw new RuntimeException("MoneroWalletRpc instance not associated with port");
|
// close wallet and stop process
|
||||||
|
walletRpc.close(save);
|
||||||
|
walletRpc.stopProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int registerPort() throws IOException {
|
private int registerPort() throws IOException {
|
||||||
|
synchronized (registeredPorts) {
|
||||||
|
|
||||||
// register next consecutive port
|
// register next consecutive port
|
||||||
if (startPort != null) {
|
if (startPort != null) {
|
||||||
int port = startPort;
|
int port = startPort;
|
||||||
while (registeredPorts.containsKey(port)) port++;
|
while (registeredPorts.containsKey(port)) port++;
|
||||||
registeredPorts.put(port, null);
|
registeredPorts.put(port, null);
|
||||||
return port;
|
return port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// register auto-assigned port
|
// register auto-assigned port
|
||||||
else {
|
else {
|
||||||
ServerSocket socket = new ServerSocket(0); // use socket to get available port
|
int port = getLocalPort();
|
||||||
int port = socket.getLocalPort();
|
registeredPorts.put(port, null);
|
||||||
socket.close();
|
return port;
|
||||||
registeredPorts.put(port, null);
|
}
|
||||||
return port;
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterPort(int port) {
|
private void unregisterPort(int port) {
|
||||||
registeredPorts.remove(port);
|
synchronized (registeredPorts) {
|
||||||
|
registeredPorts.remove(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getLocalPort() throws IOException {
|
||||||
|
ServerSocket socket = new ServerSocket(0); // use socket to get available port
|
||||||
|
int port = socket.getLocalPort();
|
||||||
|
socket.close();
|
||||||
|
return port;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ public class XmrWalletService {
|
|||||||
private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user";
|
private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user";
|
||||||
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
|
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
|
||||||
private static final String MONERO_WALLET_NAME = "haveno_XMR";
|
private static final String MONERO_WALLET_NAME = "haveno_XMR";
|
||||||
private static final long MONERO_WALLET_SYNC_RATE = 5000l;
|
private static final long MONERO_WALLET_SYNC_PERIOD = 5000l;
|
||||||
|
|
||||||
private final CoreAccountService accountService;
|
private final CoreAccountService accountService;
|
||||||
private final CoreMoneroConnectionsService connectionsService;
|
private final CoreMoneroConnectionsService connectionsService;
|
||||||
@ -154,61 +154,57 @@ public class XmrWalletService {
|
|||||||
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
|
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean walletExists(String walletName) {
|
public boolean multisigWalletExists(String tradeId) {
|
||||||
String path = walletDir.toString() + File.separator + walletName;
|
return walletExists("xmr_multisig_trade_" + tradeId);
|
||||||
return new File(path + ".keys").exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeWallet(MoneroWallet walletRpc, boolean save) {
|
|
||||||
log.info("{}.closeWallet({}, {})", getClass(), walletRpc.getPath(), save);
|
|
||||||
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteWallet(String walletName) {
|
|
||||||
log.info("{}.deleteWallet({})", getClass(), walletName);
|
|
||||||
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
|
||||||
String path = walletDir.toString() + File.separator + walletName;
|
|
||||||
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
|
||||||
if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
|
||||||
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
|
||||||
// WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
|
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
|
||||||
public synchronized MoneroWallet createMultisigWallet(String tradeId) {
|
public MoneroWallet createMultisigWallet(String tradeId) {
|
||||||
log.info("{}.createMultisigWallet({})", getClass(), tradeId);
|
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
Trade trade = tradeManager.getOpenTrade(tradeId).get();
|
||||||
String path = "xmr_multisig_trade_" + tradeId;
|
synchronized (trade) {
|
||||||
MoneroWallet multisigWallet = null;
|
if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId());
|
||||||
multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port
|
String path = "xmr_multisig_trade_" + trade.getId();
|
||||||
multisigWallets.put(tradeId, multisigWallet);
|
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port
|
||||||
multisigWallet.startSyncing(5000l);
|
multisigWallets.put(trade.getId(), multisigWallet);
|
||||||
return multisigWallet;
|
return multisigWallet;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public MoneroWallet getMultisigWallet(String tradeId) { // TODO (woodser): synchronize per wallet id
|
|
||||||
log.info("{}.getMultisigWallet({})", getClass(), tradeId);
|
// TODO (woodser): provide progress notifications during open?
|
||||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
public MoneroWallet getMultisigWallet(String tradeId) {
|
||||||
String path = "xmr_multisig_trade_" + tradeId;
|
log.info("{}.getMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
if (!walletExists(path)) return null;
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
|
synchronized (trade) {
|
||||||
multisigWallets.put(tradeId, multisigWallet);
|
if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId());
|
||||||
multisigWallet.startSyncing(5000l); // TODO (woodser): use sync period from config. apps stall if too many multisig wallets and too short sync period
|
String path = "xmr_multisig_trade_" + trade.getId();
|
||||||
return multisigWallet;
|
if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + trade.getId());
|
||||||
}
|
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
|
||||||
|
multisigWallets.put(trade.getId(), multisigWallet);
|
||||||
public synchronized boolean deleteMultisigWallet(String tradeId) {
|
return multisigWallet;
|
||||||
log.info("{}.deleteMultisigWallet({})", getClass(), tradeId);
|
}
|
||||||
String walletName = "xmr_multisig_trade_" + tradeId;
|
}
|
||||||
if (!walletExists(walletName)) return false;
|
|
||||||
try {
|
public void closeMultisigWallet(String tradeId) {
|
||||||
closeWallet(getMultisigWallet(tradeId), false);
|
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
} catch (Exception err) {
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
// multisig wallet may not be open
|
synchronized (trade) {
|
||||||
|
if (!multisigWallets.containsKey(trade.getId())) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + trade.getId());
|
||||||
|
MoneroWallet wallet = multisigWallets.remove(trade.getId());
|
||||||
|
closeWallet(wallet, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean deleteMultisigWallet(String tradeId) {
|
||||||
|
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||||
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
|
synchronized (trade) {
|
||||||
|
String walletName = "xmr_multisig_trade_" + tradeId;
|
||||||
|
if (!walletExists(walletName)) return false;
|
||||||
|
if (multisigWallets.containsKey(trade.getId())) closeMultisigWallet(tradeId);
|
||||||
|
deleteWallet(walletName);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
deleteWallet(walletName);
|
|
||||||
multisigWallets.remove(tradeId);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||||
@ -241,6 +237,11 @@ public class XmrWalletService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean walletExists(String walletName) {
|
||||||
|
String path = walletDir.toString() + File.separator + walletName;
|
||||||
|
return new File(path + ".keys").exists();
|
||||||
|
}
|
||||||
|
|
||||||
private void tryInitMainWallet() {
|
private void tryInitMainWallet() {
|
||||||
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())) {
|
||||||
@ -288,7 +289,7 @@ public class XmrWalletService {
|
|||||||
// create wallet
|
// create wallet
|
||||||
try {
|
try {
|
||||||
walletRpc.createWallet(config);
|
walletRpc.createWallet(config);
|
||||||
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
|
walletRpc.startSyncing(MONERO_WALLET_SYNC_PERIOD);
|
||||||
return walletRpc;
|
return walletRpc;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@ -305,7 +306,7 @@ public class XmrWalletService {
|
|||||||
// open wallet
|
// open wallet
|
||||||
try {
|
try {
|
||||||
walletRpc.openWallet(config);
|
walletRpc.openWallet(config);
|
||||||
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
|
walletRpc.startSyncing(MONERO_WALLET_SYNC_PERIOD);
|
||||||
return walletRpc;
|
return walletRpc;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@ -370,7 +371,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void changeWalletPasswords(String oldPassword, String newPassword) {
|
private void changeWalletPasswords(String oldPassword, String newPassword) {
|
||||||
List<String> tradeIds = tradeManager.getTrades().stream().map(Trade::getId).collect(Collectors.toList());
|
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList());
|
||||||
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, 1 + tradeIds.size()));
|
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, 1 + tradeIds.size()));
|
||||||
pool.submit(new Runnable() {
|
pool.submit(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@ -405,6 +406,21 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void closeWallet(MoneroWallet walletRpc, boolean save) {
|
||||||
|
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save);
|
||||||
|
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc, save);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteWallet(String walletName) {
|
||||||
|
log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName);
|
||||||
|
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
||||||
|
String path = walletDir.toString() + File.separator + walletName;
|
||||||
|
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||||
|
if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||||
|
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||||
|
// WalletsSetup.deleteRollingBackup(walletName); // TODO (woodser): necessary to delete rolling backup?
|
||||||
|
}
|
||||||
|
|
||||||
private void closeAllWallets() {
|
private void closeAllWallets() {
|
||||||
|
|
||||||
// collect wallets to shutdown
|
// collect wallets to shutdown
|
||||||
|
@ -429,7 +429,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
errorMessageHandler
|
errorMessageHandler
|
||||||
);
|
);
|
||||||
|
|
||||||
placeOfferProtocols.put(offer.getId(), placeOfferProtocol);
|
synchronized (placeOfferProtocols) {
|
||||||
|
placeOfferProtocols.put(offer.getId(), placeOfferProtocol);
|
||||||
|
}
|
||||||
placeOfferProtocol.placeOffer(); // TODO (woodser): if error placing offer (e.g. bad signature), remove protocol and unfreeze trade funds
|
placeOfferProtocol.placeOffer(); // TODO (woodser): if error placing offer (e.g. bad signature), remove protocol and unfreeze trade funds
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -567,13 +569,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onRemoved(@NotNull OpenOffer openOffer, ResultHandler resultHandler, Offer offer) {
|
private void onRemoved(@NotNull OpenOffer openOffer, ResultHandler resultHandler, Offer offer) {
|
||||||
|
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
|
||||||
offer.setState(Offer.State.REMOVED);
|
offer.setState(Offer.State.REMOVED);
|
||||||
openOffer.setState(OpenOffer.State.CANCELED);
|
openOffer.setState(OpenOffer.State.CANCELED);
|
||||||
openOffers.remove(openOffer);
|
openOffers.remove(openOffer);
|
||||||
closedTradableManager.add(openOffer);
|
closedTradableManager.add(openOffer);
|
||||||
log.info("onRemoved offerId={}", offer.getId());
|
log.info("onRemoved offerId={}", offer.getId());
|
||||||
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
|
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
|
||||||
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
@ -727,10 +729,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
peer, response.getOfferId(), response.getUid());
|
peer, response.getOfferId(), response.getUid());
|
||||||
|
|
||||||
// get previously created protocol
|
// get previously created protocol
|
||||||
PlaceOfferProtocol protocol = placeOfferProtocols.get(response.getOfferId());
|
PlaceOfferProtocol protocol;
|
||||||
if (protocol == null) {
|
synchronized (placeOfferProtocols) {
|
||||||
log.warn("No place offer protocol created for offer " + response.getOfferId());
|
protocol = placeOfferProtocols.get(response.getOfferId());
|
||||||
return;
|
if (protocol == null) {
|
||||||
|
log.warn("No place offer protocol created for offer " + response.getOfferId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle response
|
// handle response
|
||||||
|
@ -114,7 +114,7 @@ public class OfferAvailabilityProtocol {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void handleOfferAvailabilityResponse(OfferAvailabilityResponse message, NodeAddress peersNodeAddress) {
|
private void handleOfferAvailabilityResponse(OfferAvailabilityResponse message, NodeAddress peersNodeAddress) {
|
||||||
log.info("Received handleOfferAvailabilityResponse from {} with offerId {} and uid {}",
|
log.info("Received OfferAvailabilityResponse from {} with offerId {} and uid {}",
|
||||||
peersNodeAddress, message.getOfferId(), message.getUid());
|
peersNodeAddress, message.getOfferId(), message.getUid());
|
||||||
|
|
||||||
stopTimeout();
|
stopTimeout();
|
||||||
|
@ -45,27 +45,26 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
|
|||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// create transaction to reserve trade
|
// synchronize on wallet to reserve key images
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
synchronized (model.getXmrWalletService().getWallet()) {
|
||||||
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
|
|
||||||
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
|
|
||||||
MoneroTxWallet reserveTx = TradeUtils.createReserveTx(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount);
|
|
||||||
|
|
||||||
// freeze reserved outputs
|
// create transaction to reserve trade
|
||||||
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
|
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
List<String> reservedKeyImages = new ArrayList<String>();
|
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
|
||||||
MoneroWallet wallet = model.getXmrWalletService().getWallet();
|
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
|
||||||
for (MoneroOutput input : reserveTx.getInputs()) {
|
MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount);
|
||||||
reservedKeyImages.add(input.getKeyImage().getHex());
|
|
||||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
// collect reserved key images // TODO (woodser): switch to proof of reserve?
|
||||||
|
List<String> reservedKeyImages = new ArrayList<String>();
|
||||||
|
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||||
|
|
||||||
|
// save offer state
|
||||||
|
// TODO (woodser): persist
|
||||||
|
model.setReserveTx(reserveTx);
|
||||||
|
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
||||||
|
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field
|
||||||
|
complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// save offer state
|
|
||||||
// TODO (woodser): persist
|
|
||||||
model.setReserveTx(reserveTx);
|
|
||||||
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
|
|
||||||
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field
|
|
||||||
complete();
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
offer.setErrorMessage("An error occurred.\n" +
|
offer.setErrorMessage("An error occurred.\n" +
|
||||||
"Error message:\n"
|
"Error message:\n"
|
||||||
|
@ -84,6 +84,7 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
|
|||||||
if (!sender.equals(arbitrator.getNodeAddress())) return;
|
if (!sender.equals(arbitrator.getNodeAddress())) return;
|
||||||
AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope();
|
AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||||
if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return;
|
if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return;
|
||||||
|
if (!ackMessage.getSourceUid().equals(request.getUid())) return;
|
||||||
if (ackMessage.isSuccess()) {
|
if (ackMessage.isSuccess()) {
|
||||||
offer.setState(Offer.State.OFFER_FEE_RESERVED);
|
offer.setState(Offer.State.OFFER_FEE_RESERVED);
|
||||||
model.getP2PService().removeDecryptedDirectMessageListener(this);
|
model.getP2PService().removeDecryptedDirectMessageListener(this);
|
||||||
|
@ -114,7 +114,7 @@ public class TakeOfferModel implements Model {
|
|||||||
this.clearModel();
|
this.clearModel();
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.paymentAccount = paymentAccount;
|
this.paymentAccount = paymentAccount;
|
||||||
this.addressEntry = btcWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING);
|
this.addressEntry = btcWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); // TODO (woodser): replace with xmr or remove
|
||||||
validateModelInputs();
|
validateModelInputs();
|
||||||
|
|
||||||
this.useSavingsWallet = useSavingsWallet;
|
this.useSavingsWallet = useSavingsWallet;
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package bisq.core.presentation;
|
package bisq.core.presentation;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.core.trade.TradeManager;
|
import bisq.core.trade.TradeManager;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@ -38,10 +39,12 @@ public class TradePresentation {
|
|||||||
public TradePresentation(TradeManager tradeManager) {
|
public TradePresentation(TradeManager tradeManager) {
|
||||||
tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> {
|
tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> {
|
||||||
long numPendingTrades = (long) newValue;
|
long numPendingTrades = (long) newValue;
|
||||||
if (numPendingTrades > 0)
|
UserThread.execute(() -> {
|
||||||
this.numPendingTrades.set(String.valueOf(numPendingTrades));
|
if (numPendingTrades > 0)
|
||||||
|
this.numPendingTrades.set(String.valueOf(numPendingTrades));
|
||||||
|
|
||||||
showPendingTradesNotification.set(numPendingTrades > 0);
|
showPendingTradesNotification.set(numPendingTrades > 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,11 +128,6 @@ public class FeeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) {
|
public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) {
|
||||||
if (feeProvider.getHttpClient().hasPendingRequest()) {
|
|
||||||
log.warn("We have a pending request open. We ignore that request. httpClient {}", feeProvider.getHttpClient());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
long now = Instant.now().getEpochSecond();
|
long now = Instant.now().getEpochSecond();
|
||||||
// We all requests only each 2 minutes
|
// We all requests only each 2 minutes
|
||||||
if (now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) {
|
if (now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) {
|
||||||
|
@ -51,6 +51,7 @@ public abstract class SupportManager {
|
|||||||
protected final CoreMoneroConnectionsService connectionService;
|
protected final CoreMoneroConnectionsService connectionService;
|
||||||
protected final CoreNotificationService notificationService;
|
protected final CoreNotificationService notificationService;
|
||||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||||
|
private final Object lock = new Object();
|
||||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||||
protected final MailboxMessageService mailboxMessageService;
|
protected final MailboxMessageService mailboxMessageService;
|
||||||
@ -69,16 +70,24 @@ public abstract class SupportManager {
|
|||||||
|
|
||||||
// We get first the message handler called then the onBootstrapped
|
// We get first the message handler called then the onBootstrapped
|
||||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||||
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
|
if (isReady()) applyDirectMessage(decryptedMessageWithPubKey);
|
||||||
// already stored
|
else {
|
||||||
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
synchronized (lock) {
|
||||||
tryApplyMessages();
|
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored
|
||||||
|
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
||||||
|
tryApplyMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
|
mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||||
// As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
|
if (isReady()) applyMailboxMessage(decryptedMessageWithPubKey);
|
||||||
// already stored
|
else {
|
||||||
decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
synchronized (lock) {
|
||||||
tryApplyMessages();
|
// As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored
|
||||||
|
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
||||||
|
tryApplyMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +147,7 @@ public abstract class SupportManager {
|
|||||||
protected void onChatMessage(ChatMessage chatMessage) {
|
protected void onChatMessage(ChatMessage chatMessage) {
|
||||||
final String tradeId = chatMessage.getTradeId();
|
final String tradeId = chatMessage.getTradeId();
|
||||||
final String uid = chatMessage.getUid();
|
final String uid = chatMessage.getUid();
|
||||||
|
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
|
||||||
boolean channelOpen = channelOpen(chatMessage);
|
boolean channelOpen = channelOpen(chatMessage);
|
||||||
if (!channelOpen) {
|
if (!channelOpen) {
|
||||||
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
|
||||||
@ -293,6 +303,11 @@ public abstract class SupportManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private boolean isReady() {
|
private boolean isReady() {
|
||||||
return allServicesInitialized &&
|
return allServicesInitialized &&
|
||||||
p2PService.isBootstrapped() &&
|
p2PService.isBootstrapped() &&
|
||||||
@ -306,29 +321,34 @@ public abstract class SupportManager {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void applyMessages() {
|
private void applyMessages() {
|
||||||
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
|
synchronized (lock) {
|
||||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
|
||||||
if (networkEnvelope instanceof SupportMessage) {
|
decryptedDirectMessageWithPubKeys.clear();
|
||||||
onSupportMessage((SupportMessage) networkEnvelope);
|
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
|
||||||
} else if (networkEnvelope instanceof AckMessage) {
|
decryptedMailboxMessageWithPubKeys.clear();
|
||||||
onAckMessage((AckMessage) networkEnvelope);
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
decryptedDirectMessageWithPubKeys.clear();
|
|
||||||
|
|
||||||
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
|
private void applyDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
|
||||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||||
log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName());
|
if (networkEnvelope instanceof SupportMessage) {
|
||||||
if (networkEnvelope instanceof SupportMessage) {
|
onSupportMessage((SupportMessage) networkEnvelope);
|
||||||
SupportMessage supportMessage = (SupportMessage) networkEnvelope;
|
} else if (networkEnvelope instanceof AckMessage) {
|
||||||
onSupportMessage(supportMessage);
|
onAckMessage((AckMessage) networkEnvelope);
|
||||||
mailboxMessageService.removeMailboxMsg(supportMessage);
|
}
|
||||||
} else if (networkEnvelope instanceof AckMessage) {
|
}
|
||||||
AckMessage ackMessage = (AckMessage) networkEnvelope;
|
|
||||||
onAckMessage(ackMessage);
|
private void applyMailboxMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
|
||||||
mailboxMessageService.removeMailboxMsg(ackMessage);
|
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||||
}
|
log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName());
|
||||||
});
|
if (networkEnvelope instanceof SupportMessage) {
|
||||||
decryptedMailboxMessageWithPubKeys.clear();
|
SupportMessage supportMessage = (SupportMessage) networkEnvelope;
|
||||||
|
onSupportMessage(supportMessage);
|
||||||
|
mailboxMessageService.removeMailboxMsg(supportMessage);
|
||||||
|
} else if (networkEnvelope instanceof AckMessage) {
|
||||||
|
AckMessage ackMessage = (AckMessage) networkEnvelope;
|
||||||
|
onAckMessage(ackMessage);
|
||||||
|
mailboxMessageService.removeMailboxMsg(ackMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ import bisq.core.proto.CoreProtoResolver;
|
|||||||
import bisq.core.support.SupportType;
|
import bisq.core.support.SupportType;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.trade.Contract;
|
import bisq.core.trade.Contract;
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.proto.ProtoUtil;
|
import bisq.common.proto.ProtoUtil;
|
||||||
import bisq.common.proto.network.NetworkPayload;
|
import bisq.common.proto.network.NetworkPayload;
|
||||||
@ -365,7 +365,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||||||
|
|
||||||
public void setState(Dispute.State disputeState) {
|
public void setState(Dispute.State disputeState) {
|
||||||
this.disputeState = disputeState;
|
this.disputeState = disputeState;
|
||||||
this.isClosedProperty.set(disputeState == State.CLOSED);
|
UserThread.execute(() -> this.isClosedProperty.set(disputeState == State.CLOSED));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDisputeResult(DisputeResult disputeResult) {
|
public void setDisputeResult(DisputeResult disputeResult) {
|
||||||
|
@ -302,67 +302,81 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String errorMessage = null;
|
|
||||||
Dispute dispute = openNewDisputeMessage.getDispute();
|
Dispute dispute = openNewDisputeMessage.getDispute();
|
||||||
|
log.info("{}.onOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||||
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
|
// Disputes from clients < 1.2.0 always have support type ARBITRATION in dispute as the field didn't exist before
|
||||||
dispute.setSupportType(openNewDisputeMessage.getSupportType());
|
dispute.setSupportType(openNewDisputeMessage.getSupportType());
|
||||||
// disputes from clients < 1.6.0 have state not set as the field didn't exist before
|
// disputes from clients < 1.6.0 have state not set as the field didn't exist before
|
||||||
dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release
|
dispute.setState(Dispute.State.NEW); // this can be removed a few months after 1.6.0 release
|
||||||
|
|
||||||
Contract contract = dispute.getContract();
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
addPriceInfoMessage(dispute, 0);
|
if (trade == null) {
|
||||||
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
synchronized (trade) {
|
||||||
if (isAgent(dispute)) {
|
|
||||||
|
|
||||||
// update arbitrator's multisig wallet
|
String errorMessage = null;
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
Contract contract = dispute.getContract();
|
||||||
multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex()));
|
addPriceInfoMessage(dispute, 0);
|
||||||
System.out.println("Arbitrator multisig wallet updated on new dispute message, current txs:");
|
|
||||||
System.out.println(multisigWallet.getTxs());
|
|
||||||
|
|
||||||
if (!disputeList.contains(dispute)) {
|
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
if (isAgent(dispute)) {
|
||||||
if (!storedDisputeOptional.isPresent()) {
|
|
||||||
disputeList.add(dispute);
|
// update arbitrator's multisig wallet
|
||||||
sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing);
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||||
} else {
|
multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex()));
|
||||||
// valid case if both have opened a dispute and agent was not online.
|
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
|
||||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
|
||||||
dispute.getTradeId());
|
// close multisig wallet
|
||||||
|
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
|
||||||
|
|
||||||
|
synchronized (disputeList) {
|
||||||
|
if (!disputeList.contains(dispute)) {
|
||||||
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
if (!storedDisputeOptional.isPresent()) {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
sendPeerOpenedDisputeMessage(dispute, contract, peersPubKeyRing);
|
||||||
|
} else {
|
||||||
|
// valid case if both have opened a dispute and agent was not online.
|
||||||
|
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||||
|
dispute.getTradeId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
|
||||||
|
log.warn(errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
|
||||||
log.warn(errorMessage);
|
log.error(errorMessage);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
|
|
||||||
log.error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the ChatMessage not the openNewDisputeMessage for the ACK
|
// We use the ChatMessage not the openNewDisputeMessage for the ACK
|
||||||
ObservableList<ChatMessage> messages = openNewDisputeMessage.getDispute().getChatMessages();
|
ObservableList<ChatMessage> messages = openNewDisputeMessage.getDispute().getChatMessages();
|
||||||
if (!messages.isEmpty()) {
|
if (!messages.isEmpty()) {
|
||||||
ChatMessage msg = messages.get(0);
|
ChatMessage msg = messages.get(0);
|
||||||
PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
||||||
sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage);
|
sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
addMediationResultMessage(dispute);
|
addMediationResultMessage(dispute);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TradeDataValidation.validatePaymentAccountPayloads(dispute);
|
TradeDataValidation.validatePaymentAccountPayloads(dispute);
|
||||||
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
||||||
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
||||||
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
|
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
|
||||||
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
|
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
|
||||||
} catch (TradeDataValidation.AddressException |
|
} catch (TradeDataValidation.AddressException |
|
||||||
TradeDataValidation.NodeAddressException |
|
TradeDataValidation.NodeAddressException |
|
||||||
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
||||||
log.error(e.toString());
|
log.error(e.toString());
|
||||||
validationExceptions.add(e);
|
validationExceptions.add(e);
|
||||||
|
}
|
||||||
|
requestPersistence();
|
||||||
}
|
}
|
||||||
requestPersistence();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not-dispute-requester receives that msg from dispute agent
|
// Not-dispute-requester receives that msg from dispute agent
|
||||||
@ -375,44 +389,49 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
|
|
||||||
String errorMessage = null;
|
String errorMessage = null;
|
||||||
Dispute dispute = peerOpenedDisputeMessage.getDispute();
|
Dispute dispute = peerOpenedDisputeMessage.getDispute();
|
||||||
|
log.info("{}.onPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||||
|
|
||||||
Optional<Trade> optionalTrade = tradeManager.getTradeById(dispute.getTradeId());
|
Optional<Trade> optionalTrade = tradeManager.getOpenTrade(dispute.getTradeId());
|
||||||
if (!optionalTrade.isPresent()) {
|
if (!optionalTrade.isPresent()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Trade trade = optionalTrade.get();
|
Trade trade = optionalTrade.get();
|
||||||
|
|
||||||
if (!isAgent(dispute)) {
|
synchronized (trade) {
|
||||||
if (!disputeList.contains(dispute)) {
|
if (!isAgent(dispute)) {
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
synchronized (disputeList) {
|
||||||
if (!storedDisputeOptional.isPresent()) {
|
if (!disputeList.contains(dispute)) {
|
||||||
disputeList.add(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
trade.setDisputeState(getDisputeStateStartedByPeer());
|
if (!storedDisputeOptional.isPresent()) {
|
||||||
tradeManager.requestPersistence();
|
disputeList.add(dispute);
|
||||||
errorMessage = null;
|
trade.setDisputeState(getDisputeStateStartedByPeer());
|
||||||
} else {
|
tradeManager.requestPersistence();
|
||||||
// valid case if both have opened a dispute and agent was not online.
|
errorMessage = null;
|
||||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
} else {
|
||||||
dispute.getTradeId());
|
// valid case if both have opened a dispute and agent was not online.
|
||||||
|
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||||
|
dispute.getTradeId());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||||
|
log.warn(errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
|
||||||
log.warn(errorMessage);
|
log.error(errorMessage);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
|
|
||||||
log.error(errorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We use the ChatMessage not the peerOpenedDisputeMessage for the ACK
|
// We use the ChatMessage not the peerOpenedDisputeMessage for the ACK
|
||||||
ObservableList<ChatMessage> messages = peerOpenedDisputeMessage.getDispute().getChatMessages();
|
ObservableList<ChatMessage> messages = peerOpenedDisputeMessage.getDispute().getChatMessages();
|
||||||
if (!messages.isEmpty()) {
|
if (!messages.isEmpty()) {
|
||||||
ChatMessage msg = messages.get(0);
|
ChatMessage msg = messages.get(0);
|
||||||
sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -425,107 +444,112 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
String updatedMultisigHex,
|
String updatedMultisigHex,
|
||||||
ResultHandler resultHandler,
|
ResultHandler resultHandler,
|
||||||
FaultHandler faultHandler) {
|
FaultHandler faultHandler) {
|
||||||
|
log.info("{}.sendOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||||
|
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
log.warn("disputes is null");
|
log.warn("disputes is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (disputeList.contains(dispute)) {
|
synchronized (disputeList) {
|
||||||
String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
if (disputeList.contains(dispute)) {
|
||||||
log.warn(msg);
|
String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
||||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
log.warn(msg);
|
||||||
return;
|
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
||||||
}
|
return;
|
||||||
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
|
||||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
|
||||||
String disputeInfo = getDisputeInfo(dispute);
|
|
||||||
String sysMsg = dispute.isSupportTicket() ?
|
|
||||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
|
|
||||||
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
|
||||||
|
|
||||||
ChatMessage chatMessage = new ChatMessage(
|
|
||||||
getSupportType(),
|
|
||||||
dispute.getTradeId(),
|
|
||||||
pubKeyRing.hashCode(),
|
|
||||||
false,
|
|
||||||
Res.get("support.systemMsg", sysMsg),
|
|
||||||
p2PService.getAddress());
|
|
||||||
chatMessage.setSystemMessage(true);
|
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
|
||||||
if (!reOpen) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute,
|
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||||
p2PService.getAddress(),
|
String disputeInfo = getDisputeInfo(dispute);
|
||||||
UUID.randomUUID().toString(),
|
String sysMsg = dispute.isSupportTicket() ?
|
||||||
getSupportType(),
|
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
|
||||||
updatedMultisigHex);
|
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
||||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
|
||||||
"chatMessage.uid={}",
|
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
|
||||||
chatMessage.getUid());
|
|
||||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
|
||||||
dispute.getAgentPubKeyRing(),
|
|
||||||
openNewDisputeMessage,
|
|
||||||
new SendMailboxMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
|
||||||
"chatMessage.uid={}",
|
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
|
||||||
chatMessage.getUid());
|
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
ChatMessage chatMessage = new ChatMessage(
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
getSupportType(),
|
||||||
chatMessage.setArrived(true);
|
dispute.getTradeId(),
|
||||||
requestPersistence();
|
pubKeyRing.hashCode(),
|
||||||
resultHandler.handleResult();
|
false,
|
||||||
|
Res.get("support.systemMsg", sysMsg),
|
||||||
|
p2PService.getAddress());
|
||||||
|
chatMessage.setSystemMessage(true);
|
||||||
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
|
if (!reOpen) {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||||
|
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute,
|
||||||
|
p2PService.getAddress(),
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
getSupportType(),
|
||||||
|
updatedMultisigHex);
|
||||||
|
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
|
"chatMessage.uid={}",
|
||||||
|
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
|
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||||
|
chatMessage.getUid());
|
||||||
|
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||||
|
dispute.getAgentPubKeyRing(),
|
||||||
|
openNewDisputeMessage,
|
||||||
|
new SendMailboxMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
|
"chatMessage.uid={}",
|
||||||
|
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
|
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||||
|
chatMessage.getUid());
|
||||||
|
|
||||||
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
|
chatMessage.setArrived(true);
|
||||||
|
requestPersistence();
|
||||||
|
resultHandler.handleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStoredInMailbox() {
|
||||||
|
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
|
"chatMessage.uid={}",
|
||||||
|
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
|
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||||
|
chatMessage.getUid());
|
||||||
|
|
||||||
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
|
chatMessage.setStoredInMailbox(true);
|
||||||
|
requestPersistence();
|
||||||
|
resultHandler.handleResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
|
"chatMessage.uid={}, errorMessage={}",
|
||||||
|
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||||
|
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||||
|
chatMessage.getUid(), errorMessage);
|
||||||
|
|
||||||
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
|
chatMessage.setSendMessageError(errorMessage);
|
||||||
|
requestPersistence();
|
||||||
|
faultHandler.handleFault("Sending dispute message failed: " +
|
||||||
|
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
@Override
|
} else {
|
||||||
public void onStoredInMailbox() {
|
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
||||||
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
"TradeId = " + dispute.getTradeId();
|
||||||
"chatMessage.uid={}",
|
log.warn(msg);
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
}
|
||||||
chatMessage.getUid());
|
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
|
||||||
chatMessage.setStoredInMailbox(true);
|
|
||||||
requestPersistence();
|
|
||||||
resultHandler.handleResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
|
||||||
"chatMessage.uid={}, errorMessage={}",
|
|
||||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
|
||||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
|
||||||
chatMessage.getUid(), errorMessage);
|
|
||||||
|
|
||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
|
||||||
chatMessage.setSendMessageError(errorMessage);
|
|
||||||
requestPersistence();
|
|
||||||
faultHandler.handleFault("Sending dispute message failed: " +
|
|
||||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
|
||||||
"TradeId = " + dispute.getTradeId();
|
|
||||||
log.warn(msg);
|
|
||||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -533,6 +557,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
||||||
Contract contractFromOpener,
|
Contract contractFromOpener,
|
||||||
PubKeyRing pubKeyRing) {
|
PubKeyRing pubKeyRing) {
|
||||||
|
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
||||||
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
||||||
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
||||||
// message and not skip the system message of the peer as it would be the case if we have created the system msg
|
// message and not skip the system message of the peer as it would be the case if we have created the system msg
|
||||||
@ -604,7 +629,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
|
|
||||||
addPriceInfoMessage(dispute, 0);
|
addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
disputeList.add(dispute);
|
synchronized (disputeList) {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
}
|
||||||
|
|
||||||
// We mirrored dispute already!
|
// We mirrored dispute already!
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
@ -826,9 +853,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Trade> findTrade(Dispute dispute) {
|
public Optional<Trade> findTrade(Dispute dispute) {
|
||||||
Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId());
|
Optional<Trade> retVal = tradeManager.getOpenTrade(dispute.getTradeId());
|
||||||
if (!retVal.isPresent()) {
|
if (!retVal.isPresent()) {
|
||||||
retVal = closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(dispute.getTradeId())).findFirst();
|
retVal = tradeManager.getClosedTrade(dispute.getTradeId());
|
||||||
}
|
}
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
@ -120,8 +120,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
@Override
|
@Override
|
||||||
public void onSupportMessage(SupportMessage message) {
|
public void onSupportMessage(SupportMessage message) {
|
||||||
if (canProcessMessage(message)) {
|
if (canProcessMessage(message)) {
|
||||||
log.info("Received {} with tradeId {} and uid {}",
|
log.info("Received {} from {} with tradeId {} and uid {}",
|
||||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
|
||||||
|
|
||||||
if (message instanceof OpenNewDisputeMessage) {
|
if (message instanceof OpenNewDisputeMessage) {
|
||||||
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
||||||
@ -195,9 +195,10 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(disputeResult.getTradeId());
|
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(disputeResult.getTradeId());
|
||||||
|
|
||||||
String tradeId = disputeResult.getTradeId();
|
String tradeId = disputeResult.getTradeId();
|
||||||
|
log.info("{}.onDisputeResultMessage() for trade {}", getClass().getSimpleName(), disputeResult.getTradeId());
|
||||||
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
Optional<Dispute> disputeOptional = findDispute(disputeResult);
|
||||||
String uid = disputeResultMessage.getUid();
|
String uid = disputeResultMessage.getUid();
|
||||||
if (!disputeOptional.isPresent()) {
|
if (!disputeOptional.isPresent()) {
|
||||||
@ -239,7 +240,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
String errorMessage = null;
|
String errorMessage = null;
|
||||||
boolean success = true;
|
boolean success = true;
|
||||||
boolean requestUpdatedPayoutTx = false;
|
boolean requestUpdatedPayoutTx = false;
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
try {
|
try {
|
||||||
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
|
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
|
||||||
@ -269,7 +269,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
} else {
|
} else {
|
||||||
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
|
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
|
||||||
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
|
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
|
||||||
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx();
|
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); // TODO (woodser): payout tx is transient so won't exist after restart?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -334,7 +334,14 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
||||||
|
|
||||||
// If dispute opener's peer is co-signer, send updated multisig hex to arbitrator to receive updated payout tx
|
// If dispute opener's peer is co-signer, send updated multisig hex to arbitrator to receive updated payout tx
|
||||||
if (requestUpdatedPayoutTx) sendArbitratorPayoutTxRequest(multisigWallet.getMultisigHex(), dispute, contract);
|
if (requestUpdatedPayoutTx) {
|
||||||
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
|
synchronized (trade) {
|
||||||
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // TODO (woodser): this is closed after sending ArbitratorPayoutTxRequest to arbitrator which opens and syncs multisig and responds with signed dispute tx. more efficient way is to include with arbitrator-signed dispute tx with dispute result?
|
||||||
|
sendArbitratorPayoutTxRequest(multisigWallet.getMultisigHex(), dispute, contract);
|
||||||
|
xmrWalletService.closeMultisigWallet(tradeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
@ -344,136 +351,218 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
||||||
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
||||||
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
||||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
if (!disputeOptional.isPresent()) {
|
|
||||||
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
synchronized (trade) {
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
|
||||||
// We delay 3 sec. to be sure the close msg gets added first
|
// get dispute and trade
|
||||||
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3);
|
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||||
delayMsgMap.put(uid, timer);
|
if (!disputeOptional.isPresent()) {
|
||||||
} else {
|
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
||||||
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
"That should never happen. TradeId = " + tradeId);
|
// We delay 3 sec. to be sure the close msg gets added first
|
||||||
|
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3);
|
||||||
|
delayMsgMap.put(uid, timer);
|
||||||
|
} else {
|
||||||
|
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
|
||||||
|
"That should never happen. TradeId = " + tradeId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
Dispute dispute = disputeOptional.get();
|
||||||
|
|
||||||
|
Contract contract = dispute.getContract();
|
||||||
|
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
|
||||||
|
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||||
|
|
||||||
|
cleanupRetryMap(uid);
|
||||||
|
|
||||||
|
// update multisig wallet
|
||||||
|
if (xmrWalletService.multisigWalletExists(tradeId)) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
|
||||||
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||||
|
multisigWallet.importMultisigHex(Arrays.asList(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex()));
|
||||||
|
MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
|
||||||
|
xmrWalletService.closeMultisigWallet(tradeId);
|
||||||
|
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
|
||||||
|
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs());
|
||||||
|
// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx");
|
||||||
|
// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet());
|
||||||
|
|
||||||
|
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
|
||||||
|
sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
|
||||||
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
Dispute dispute = disputeOptional.get();
|
|
||||||
Contract contract = dispute.getContract();
|
|
||||||
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
|
|
||||||
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
|
||||||
|
|
||||||
cleanupRetryMap(uid);
|
|
||||||
|
|
||||||
// update multisig wallet
|
|
||||||
// TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
|
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
|
||||||
if (multisigWallet != null) {
|
|
||||||
multisigWallet.importMultisigHex(Arrays.asList(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex()));
|
|
||||||
MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
|
|
||||||
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
|
|
||||||
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// System.out.println("LOSER'S VIEW OF MULTISIG WALLET (SHOULD INCLUDE PAYOUT TX):\n" + multisigWallet.getTxs());
|
|
||||||
// if (multisigWallet.getTxs().size() != 3) throw new RuntimeException("Loser's multisig wallet does not include record of payout tx");
|
|
||||||
// Transaction committedDisputePayoutTx = WalletService.maybeAddNetworkTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction(), btcWalletService.getWallet());
|
|
||||||
|
|
||||||
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
|
|
||||||
sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
|
|
||||||
requestPersistence();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published
|
// Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published
|
||||||
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
|
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
|
||||||
|
log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName());
|
||||||
String tradeId = request.getTradeId();
|
String tradeId = request.getTradeId();
|
||||||
Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get();
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
synchronized (trade) {
|
||||||
Contract contract = dispute.getContract();
|
Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get();
|
||||||
|
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
||||||
|
Contract contract = dispute.getContract();
|
||||||
|
|
||||||
// verify sender is co-signer and receiver is arbitrator
|
// verify sender is co-signer and receiver is arbitrator
|
||||||
System.out.println("Any of these null???"); // TODO (woodser): NPE if dispute opener's peer-as-cosigner's ticket is closed first
|
System.out.println("Any of these null???"); // TODO (woodser): NPE if dispute opener's peer-as-cosigner's ticket is closed first
|
||||||
System.out.println(disputeResult);
|
System.out.println(disputeResult);
|
||||||
System.out.println(disputeResult.getWinner());
|
System.out.println(disputeResult.getWinner());
|
||||||
System.out.println(contract.getBuyerNodeAddress());
|
System.out.println(contract.getBuyerNodeAddress());
|
||||||
System.out.println(contract.getSellerNodeAddress());
|
System.out.println(contract.getSellerNodeAddress());
|
||||||
boolean senderIsWinner = (disputeResult.getWinner() == Winner.BUYER && contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress())) || (disputeResult.getWinner() == Winner.SELLER && contract.getSellerNodeAddress().equals(request.getSenderNodeAddress()));
|
boolean senderIsWinner = (disputeResult.getWinner() == Winner.BUYER && contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress())) || (disputeResult.getWinner() == Winner.SELLER && contract.getSellerNodeAddress().equals(request.getSenderNodeAddress()));
|
||||||
boolean senderIsCosigner = senderIsWinner || disputeResult.isLoserPublisher();
|
boolean senderIsCosigner = senderIsWinner || disputeResult.isLoserPublisher();
|
||||||
boolean receiverIsArbitrator = pubKeyRing.equals(dispute.getAgentPubKeyRing());
|
boolean receiverIsArbitrator = pubKeyRing.equals(dispute.getAgentPubKeyRing());
|
||||||
|
|
||||||
System.out.println("TESTING PUB KEY RINGS");
|
System.out.println("TESTING PUB KEY RINGS");
|
||||||
System.out.println(pubKeyRing);
|
System.out.println(pubKeyRing);
|
||||||
System.out.println(dispute.getAgentPubKeyRing());
|
System.out.println(dispute.getAgentPubKeyRing());
|
||||||
System.out.println("Receiver is arbitrator: " + receiverIsArbitrator);
|
System.out.println("Receiver is arbitrator: " + receiverIsArbitrator);
|
||||||
|
|
||||||
if (!senderIsCosigner) {
|
if (!senderIsCosigner) {
|
||||||
log.warn("Received ArbitratorPayoutTxRequest but sender is not co-signer for trade id " + tradeId);
|
log.warn("Received ArbitratorPayoutTxRequest but sender is not co-signer for trade id " + tradeId);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
if (!receiverIsArbitrator) {
|
|
||||||
log.warn("Received ArbitratorPayoutTxRequest but receiver is not arbitrator for trade id " + tradeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update arbitrator's multisig wallet with co-signer's multisig hex
|
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
|
||||||
try {
|
|
||||||
multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// create updated payout tx
|
|
||||||
MoneroTxWallet payoutTx = arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
|
||||||
System.out.println("Arbitrator created updated payout tx for co-signer!!!");
|
|
||||||
System.out.println(payoutTx);
|
|
||||||
|
|
||||||
// send updated payout tx to sender
|
|
||||||
PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
|
||||||
ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse(
|
|
||||||
tradeId,
|
|
||||||
p2PService.getAddress(),
|
|
||||||
UUID.randomUUID().toString(),
|
|
||||||
SupportType.ARBITRATION,
|
|
||||||
payoutTx.getTxSet().getMultisigTxHex());
|
|
||||||
log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
|
||||||
p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(),
|
|
||||||
senderPubKeyRing,
|
|
||||||
response,
|
|
||||||
new SendDirectMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
|
||||||
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
|
||||||
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid(), errorMessage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
if (!receiverIsArbitrator) {
|
||||||
|
log.warn("Received ArbitratorPayoutTxRequest but receiver is not arbitrator for trade id " + tradeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update arbitrator's multisig wallet with co-signer's multisig hex
|
||||||
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||||
|
try {
|
||||||
|
multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create updated payout tx
|
||||||
|
MoneroTxWallet payoutTx = arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
||||||
|
System.out.println("Arbitrator created updated payout tx for co-signer!!!");
|
||||||
|
System.out.println(payoutTx);
|
||||||
|
|
||||||
|
// close multisig wallet
|
||||||
|
xmrWalletService.closeMultisigWallet(tradeId);
|
||||||
|
|
||||||
|
// send updated payout tx to sender
|
||||||
|
PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
||||||
|
ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse(
|
||||||
|
tradeId,
|
||||||
|
p2PService.getAddress(),
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
SupportType.ARBITRATION,
|
||||||
|
payoutTx.getTxSet().getMultisigTxHex());
|
||||||
|
log.info("Send {} to peer {}. tradeId={}, uid={}", response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
||||||
|
p2PService.sendEncryptedDirectMessage(request.getSenderNodeAddress(),
|
||||||
|
senderPubKeyRing,
|
||||||
|
response,
|
||||||
|
new SendDirectMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
||||||
|
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
||||||
|
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid(), errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispute opener's peer receives updated payout tx after providing updated multisig hex (if co-signer)
|
// Dispute opener's peer receives updated payout tx after providing updated multisig hex (if co-signer)
|
||||||
private void onArbitratorPayoutTxResponse(ArbitratorPayoutTxResponse response) {
|
private void onArbitratorPayoutTxResponse(ArbitratorPayoutTxResponse response) {
|
||||||
|
log.info("{}.onArbitratorPayoutTxResponse()", getClass().getSimpleName());
|
||||||
|
|
||||||
// gather and verify trade info // TODO (woodser): verify response is from arbitrator, etc
|
// gather and verify trade info // TODO (woodser): verify response is from arbitrator, etc
|
||||||
String tradeId = response.getTradeId();
|
String tradeId = response.getTradeId();
|
||||||
|
Trade trade = tradeManager.getTrade(tradeId);
|
||||||
|
synchronized (trade) {
|
||||||
|
|
||||||
// verify and sign dispute payout tx
|
// verify and sign dispute payout tx
|
||||||
MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex());
|
MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex());
|
||||||
|
|
||||||
// process fully signed payout tx (publish, notify peer, etc)
|
// process fully signed payout tx (publish, notify peer, etc)
|
||||||
onTraderSignedDisputePayoutTx(tradeId, signedPayoutTx);
|
onTraderSignedDisputePayoutTx(tradeId, signedPayoutTx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) {
|
||||||
|
|
||||||
|
// gather trade info
|
||||||
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
||||||
|
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||||
|
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
||||||
|
Dispute dispute = disputeOptional.get();
|
||||||
|
Contract contract = dispute.getContract();
|
||||||
|
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
||||||
|
|
||||||
|
// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
|
||||||
|
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
|
||||||
|
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
|
||||||
|
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
||||||
|
|
||||||
|
// parse arbitrator-signed payout tx
|
||||||
|
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||||
|
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
||||||
|
MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0);
|
||||||
|
log.info("Received updated multisig hex and partially signed payout tx from arbitrator:\n" + arbitratorSignedPayoutTx);
|
||||||
|
|
||||||
|
// verify payout tx has 1 or 2 destinations
|
||||||
|
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
||||||
|
if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations");
|
||||||
|
|
||||||
|
// get buyer and seller destinations (order not preserved)
|
||||||
|
List<MoneroDestination> destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations();
|
||||||
|
boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
|
||||||
|
MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null;
|
||||||
|
MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0);
|
||||||
|
|
||||||
|
// verify payout addresses
|
||||||
|
if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
|
||||||
|
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
||||||
|
|
||||||
|
// verify change address is multisig's primary address
|
||||||
|
if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
||||||
|
|
||||||
|
// verify sum of outputs = destination amounts + change amount
|
||||||
|
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
|
||||||
|
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
||||||
|
|
||||||
|
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
|
||||||
|
|
||||||
|
// verify winner and loser payout amounts
|
||||||
|
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
|
||||||
|
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
||||||
|
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
||||||
|
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
|
||||||
|
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
|
||||||
|
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
|
||||||
|
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
|
||||||
|
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
||||||
|
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
||||||
|
|
||||||
|
// update multisig wallet from arbitrator
|
||||||
|
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
|
||||||
|
|
||||||
|
// sign arbitrator-signed payout tx
|
||||||
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||||
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||||
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
|
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
|
return parsedTxSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) {
|
private void onTraderSignedDisputePayoutTx(String tradeId, MoneroTxSet txSet) {
|
||||||
|
|
||||||
// gather trade info
|
// gather trade info
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
|
||||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||||
if (!disputeOptional.isPresent()) {
|
if (!disputeOptional.isPresent()) {
|
||||||
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
||||||
@ -481,10 +570,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
}
|
}
|
||||||
Dispute dispute = disputeOptional.get();
|
Dispute dispute = disputeOptional.get();
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
Trade trade = tradeManager.getTradeById(tradeId).get();
|
Trade trade = tradeManager.getOpenTrade(tradeId).get();
|
||||||
|
|
||||||
// submit fully signed payout tx to the network
|
// submit fully signed payout tx to the network
|
||||||
multisigWallet.submitMultisigTxHex(txSet.getMultisigTxHex());
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // closed when trade completed (TradeManager.onTradeCompleted())
|
||||||
|
List<String> txHashes = multisigWallet.submitMultisigTxHex(txSet.getMultisigTxHex());
|
||||||
|
txSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||||
@ -505,7 +596,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
||||||
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress {}", peersNodeAddress);
|
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress {}", peersNodeAddress);
|
||||||
PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(updatedMultisigHex,
|
PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(updatedMultisigHex,
|
||||||
payoutTxHex,
|
payoutTxHex,
|
||||||
dispute.getTradeId(),
|
dispute.getTradeId(),
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
@ -539,7 +630,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
|
|
||||||
private void updateTradeOrOpenOfferManager(String tradeId) {
|
private void updateTradeOrOpenOfferManager(String tradeId) {
|
||||||
// set state after payout as we call swapTradeEntryToAvailableEntry
|
// set state after payout as we call swapTradeEntryToAvailableEntry
|
||||||
if (tradeManager.getTradeById(tradeId).isPresent()) {
|
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
|
||||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
|
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
} else {
|
} else {
|
||||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
||||||
@ -622,71 +713,4 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
||||||
return payoutTx;
|
return payoutTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) {
|
|
||||||
|
|
||||||
// gather trade info
|
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
|
||||||
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
|
||||||
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
|
||||||
Dispute dispute = disputeOptional.get();
|
|
||||||
Contract contract = dispute.getContract();
|
|
||||||
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
|
|
||||||
|
|
||||||
// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
|
|
||||||
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
|
|
||||||
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
|
|
||||||
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
|
||||||
|
|
||||||
// parse arbitrator-signed payout tx
|
|
||||||
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
|
||||||
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
|
|
||||||
MoneroTxWallet arbitratorSignedPayoutTx = parsedTxSet.getTxs().get(0);
|
|
||||||
System.out.println("Parsed arbitrator-signed payout tx:\n" + arbitratorSignedPayoutTx);
|
|
||||||
|
|
||||||
// verify payout tx has 1 or 2 destinations
|
|
||||||
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
|
|
||||||
if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations");
|
|
||||||
|
|
||||||
// get buyer and seller destinations (order not preserved)
|
|
||||||
List<MoneroDestination> destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations();
|
|
||||||
boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
|
|
||||||
MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null;
|
|
||||||
MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0);
|
|
||||||
|
|
||||||
// verify payout addresses
|
|
||||||
if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
|
|
||||||
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
|
||||||
|
|
||||||
// verify change address is multisig's primary address
|
|
||||||
if (arbitratorSignedPayoutTx.getChangeAddress() != null && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
|
||||||
|
|
||||||
// verify sum of outputs = destination amounts + change amount
|
|
||||||
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
|
|
||||||
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
|
||||||
|
|
||||||
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
|
|
||||||
|
|
||||||
// verify winner and loser payout amounts
|
|
||||||
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
|
|
||||||
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
|
||||||
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
|
||||||
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
|
|
||||||
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
|
|
||||||
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
|
|
||||||
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
|
|
||||||
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
|
||||||
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
|
||||||
|
|
||||||
// update multisig wallet from arbitrator
|
|
||||||
System.out.println("Updating multisig hex from arbitrator");
|
|
||||||
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
|
|
||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
|
||||||
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
|
||||||
return parsedTxSet;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -134,7 +134,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||||||
@Override
|
@Override
|
||||||
public void cleanupDisputes() {
|
public void cleanupDisputes() {
|
||||||
disputeListService.cleanupDisputes(tradeId -> {
|
disputeListService.cleanupDisputes(tradeId -> {
|
||||||
tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null)
|
tradeManager.getOpenTrade(tradeId).filter(trade -> trade.getPayoutTx() != null)
|
||||||
.ifPresent(trade -> {
|
.ifPresent(trade -> {
|
||||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
|
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
|
||||||
});
|
});
|
||||||
@ -197,7 +197,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||||||
|
|
||||||
dispute.setDisputeResult(disputeResult);
|
dispute.setDisputeResult(disputeResult);
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
|
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(tradeId);
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED ||
|
if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED ||
|
||||||
|
@ -200,7 +200,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||||||
|
|
||||||
dispute.setDisputeResult(disputeResult);
|
dispute.setDisputeResult(disputeResult);
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
|
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(tradeId);
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED ||
|
if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED ||
|
||||||
@ -215,7 +215,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||||
|
|
||||||
// set state after payout as we call swapTradeEntryToAvailableEntry
|
// set state after payout as we call swapTradeEntryToAvailableEntry
|
||||||
if (tradeManager.getTradeById(tradeId).isPresent()) {
|
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
|
||||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
|
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
|
||||||
} else {
|
} else {
|
||||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
||||||
|
@ -83,7 +83,7 @@ public class TraderChatManager extends SupportManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||||
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
|
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
|
||||||
if (trade.getContract() != null) {
|
if (trade.getContract() != null) {
|
||||||
return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get());
|
return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get());
|
||||||
} else {
|
} else {
|
||||||
@ -94,7 +94,7 @@ public class TraderChatManager extends SupportManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
|
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
|
||||||
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
|
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
|
||||||
if (trade.getContract() != null) {
|
if (trade.getContract() != null) {
|
||||||
return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get());
|
return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get());
|
||||||
} else {
|
} else {
|
||||||
@ -112,12 +112,12 @@ public class TraderChatManager extends SupportManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean channelOpen(ChatMessage message) {
|
public boolean channelOpen(ChatMessage message) {
|
||||||
return tradeManager.getTradeById(message.getTradeId()).isPresent();
|
return tradeManager.getOpenTrade(message.getTradeId()).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addAndPersistChatMessage(ChatMessage message) {
|
public void addAndPersistChatMessage(ChatMessage message) {
|
||||||
tradeManager.getTradeById(message.getTradeId()).ifPresent(trade -> {
|
tradeManager.getOpenTrade(message.getTradeId()).ifPresent(trade -> {
|
||||||
ObservableList<ChatMessage> chatMessages = trade.getChatMessages();
|
ObservableList<ChatMessage> chatMessages = trade.getChatMessages();
|
||||||
if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) {
|
if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) {
|
||||||
if (chatMessages.isEmpty()) {
|
if (chatMessages.isEmpty()) {
|
||||||
|
@ -54,10 +54,12 @@ public final class TradableList<T extends Tradable> extends PersistableListAsObs
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Message toProtoMessage() {
|
public Message toProtoMessage() {
|
||||||
return protobuf.PersistableEnvelope.newBuilder()
|
synchronized (getList()) {
|
||||||
.setTradableList(protobuf.TradableList.newBuilder()
|
return protobuf.PersistableEnvelope.newBuilder()
|
||||||
.addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class)))
|
.setTradableList(protobuf.TradableList.newBuilder()
|
||||||
.build();
|
.addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class)))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TradableList<Tradable> fromProto(protobuf.TradableList proto,
|
public static TradableList<Tradable> fromProto(protobuf.TradableList proto,
|
||||||
|
@ -63,6 +63,7 @@ import javafx.collections.FXCollections;
|
|||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@ -82,8 +83,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
|
|
||||||
import monero.common.MoneroError;
|
import monero.common.MoneroError;
|
||||||
import monero.daemon.MoneroDaemon;
|
import monero.daemon.MoneroDaemon;
|
||||||
|
import monero.daemon.model.MoneroTx;
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroOutputWallet;
|
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import monero.wallet.model.MoneroWalletListener;
|
import monero.wallet.model.MoneroWalletListener;
|
||||||
|
|
||||||
@ -99,9 +100,11 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
// #################### Phase PREPARATION
|
// #################### Phase INIT
|
||||||
// When trade protocol starts no funds are on stake
|
// When trade protocol starts no funds are on stake
|
||||||
PREPARATION(Phase.INIT),
|
PREPARATION(Phase.INIT),
|
||||||
|
CONTRACT_SIGNATURE_REQUESTED(Phase.INIT), // TODO (woodser): add more states for initializing multisig, etc to support trade initialization notifications
|
||||||
|
CONTRACT_SIGNED(Phase.INIT),
|
||||||
|
|
||||||
// At first part maker/taker have different roles
|
// At first part maker/taker have different roles
|
||||||
// taker perspective
|
// taker perspective
|
||||||
@ -206,9 +209,9 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
public enum Phase {
|
public enum Phase {
|
||||||
INIT,
|
INIT,
|
||||||
TAKER_FEE_PUBLISHED,
|
TAKER_FEE_PUBLISHED, // TODO (woodser): remove unused phases
|
||||||
DEPOSIT_PUBLISHED,
|
DEPOSIT_PUBLISHED,
|
||||||
DEPOSIT_CONFIRMED,
|
DEPOSIT_CONFIRMED, // TODO (woodser): rename to or add DEPOSIT_UNLOCKED
|
||||||
FIAT_SENT,
|
FIAT_SENT,
|
||||||
FIAT_RECEIVED,
|
FIAT_RECEIVED,
|
||||||
PAYOUT_PUBLISHED,
|
PAYOUT_PUBLISHED,
|
||||||
@ -463,8 +466,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
transient MoneroWalletListener depositTxListener;
|
transient MoneroWalletListener depositTxListener;
|
||||||
transient Boolean makerDepositLocked; // null when unknown, true while locked, false when unlocked
|
transient Boolean makerDepositLocked; // null when unknown, true while locked, false when unlocked
|
||||||
transient Boolean takerDepositLocked;
|
transient Boolean takerDepositLocked;
|
||||||
transient private MoneroTxWallet makerDepositTx;
|
transient private MoneroTx makerDepositTx;
|
||||||
transient private MoneroTxWallet takerDepositTx;
|
transient private MoneroTx takerDepositTx;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor, initialization
|
// Constructor, initialization
|
||||||
@ -696,23 +699,14 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
else throw new RuntimeException("Unknown trade type: " + this.getClass().getName());
|
else throw new RuntimeException("Unknown trade type: " + this.getClass().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet.
|
/**
|
||||||
void updateDepositTxFromWallet() {
|
* Listen for deposit transactions to unlock and then apply the transactions.
|
||||||
if (getMakerDepositTx() != null && getTakerDepositTx() != null) {
|
*
|
||||||
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(getId());
|
* TODO: adopt for general purpose scheduling
|
||||||
applyDepositTxs(multisigWallet.getTx(getMakerDepositTx().getHash()), multisigWallet.getTx(getTakerDepositTx().getHash()));
|
* TODO: check and notify if deposits are dropped due to re-org
|
||||||
}
|
*/
|
||||||
}
|
public void listenForDepositTxs() {
|
||||||
|
log.info("Listening for deposit txs to unlock for trade {}", getId());
|
||||||
public void applyDepositTxs(MoneroTxWallet makerDepositTx, MoneroTxWallet takerDepositTx) {
|
|
||||||
this.makerDepositTx = makerDepositTx;
|
|
||||||
this.takerDepositTx = takerDepositTx;
|
|
||||||
if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) {
|
|
||||||
setConfirmedState(); // TODO (woodser): bisq "confirmed" = xmr unlocked after 10 confirmations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setupDepositTxsListener() {
|
|
||||||
|
|
||||||
// ignore if already listening
|
// ignore if already listening
|
||||||
if (depositTxListener != null) {
|
if (depositTxListener != null) {
|
||||||
@ -720,48 +714,86 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create listener for deposit transactions
|
// get daemon and primary wallet
|
||||||
MoneroWallet multisigWallet = processModel.getXmrWalletService().getMultisigWallet(getId());
|
MoneroDaemon daemon = processModel.getXmrWalletService().getDaemon();
|
||||||
|
MoneroWallet havenoWallet = processModel.getXmrWalletService().getWallet();
|
||||||
|
|
||||||
|
// fetch deposit txs from daemon
|
||||||
|
List<MoneroTx> txs = daemon.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()), true);
|
||||||
|
|
||||||
|
// handle deposit txs seen
|
||||||
|
if (txs.size() == 2) {
|
||||||
|
|
||||||
|
// update state
|
||||||
|
setState(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK);
|
||||||
|
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||||
|
makerDepositTx = makerFirst ? txs.get(0) : txs.get(1);
|
||||||
|
takerDepositTx = makerFirst ? txs.get(1) : txs.get(0);
|
||||||
|
|
||||||
|
// check if deposit txs unlocked
|
||||||
|
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) {
|
||||||
|
long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + 9;
|
||||||
|
if (havenoWallet.getHeight() >= unlockHeight) {
|
||||||
|
setConfirmedState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create block listener
|
||||||
depositTxListener = processModel.getXmrWalletService().new HavenoWalletListener(new MoneroWalletListener() { // TODO (woodser): separate into own class file
|
depositTxListener = processModel.getXmrWalletService().new HavenoWalletListener(new MoneroWalletListener() { // TODO (woodser): separate into own class file
|
||||||
|
|
||||||
|
Long unlockHeight = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOutputReceived(MoneroOutputWallet output) {
|
public void onNewBlock(long height) {
|
||||||
|
|
||||||
// ignore if no longer listening
|
// ignore if no longer listening
|
||||||
if (depositTxListener == null) return;
|
if (depositTxListener == null) return;
|
||||||
|
|
||||||
// TODO (woodser): remove this
|
// ignore if before unlock height
|
||||||
if (output.getTx().isConfirmed() && output.getTx().isLocked() && (processModel.getMaker().getDepositTxHash().equals(output.getTx().getHash()) || processModel.getTaker().getDepositTxHash().equals(output.getTx().getHash()))) {
|
if (unlockHeight != null && height < unlockHeight) return;
|
||||||
System.out.println("Deposit output for tx " + output.getTx().getHash() + " is confirmed at height " + output.getTx().getHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
// update locked state
|
// fetch txs from daemon
|
||||||
if (output.getTx().getHash().equals(processModel.getMaker().getDepositTxHash())) makerDepositLocked = output.getTx().isLocked();
|
List<MoneroTx> txs = daemon.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()), true);
|
||||||
else if (output.getTx().getHash().equals(processModel.getTaker().getDepositTxHash())) takerDepositLocked = output.getTx().isLocked();
|
|
||||||
|
|
||||||
// deposit txs seen when both locked states seen
|
// ignore if deposit txs not seen
|
||||||
if (makerDepositLocked != null && takerDepositLocked != null) {
|
if (txs.size() != 2) return;
|
||||||
|
|
||||||
|
// update deposit txs
|
||||||
|
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||||
|
makerDepositTx = makerFirst ? txs.get(0) : txs.get(1);
|
||||||
|
takerDepositTx = makerFirst ? txs.get(1) : txs.get(0);
|
||||||
|
|
||||||
|
// update state when deposit txs seen
|
||||||
|
if (txs.size() == 2) {
|
||||||
setState(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK);
|
setState(this instanceof MakerTrade ? Trade.State.MAKER_SAW_DEPOSIT_TX_IN_NETWORK : Trade.State.TAKER_SAW_DEPOSIT_TX_IN_NETWORK);
|
||||||
}
|
}
|
||||||
|
|
||||||
// confirm trade and update ui when both deposits unlock
|
// compute unlock height
|
||||||
if (Boolean.FALSE.equals(makerDepositLocked) && Boolean.FALSE.equals(takerDepositLocked)) {
|
if (unlockHeight == null && txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) {
|
||||||
System.out.println("Multisig deposit txs unlocked!");
|
unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + 9;
|
||||||
applyDepositTxs(multisigWallet.getTx(processModel.getMaker().getDepositTxHash()), multisigWallet.getTx(processModel.getTaker().getDepositTxHash()));
|
}
|
||||||
multisigWallet.removeListener(depositTxListener); // remove listener when notified
|
|
||||||
|
// check if txs unlocked
|
||||||
|
if (unlockHeight != null && height == unlockHeight) {
|
||||||
|
log.info("Multisig deposits unlocked for trade {}", getId());
|
||||||
|
setConfirmedState(); // TODO (woodser): bisq "confirmed" = xmr unlocked after 10 confirmations
|
||||||
|
havenoWallet.removeListener(depositTxListener); // remove listener when notified
|
||||||
depositTxListener = null; // prevent re-applying trade state in subsequent requests
|
depositTxListener = null; // prevent re-applying trade state in subsequent requests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// register wallet listener
|
// register wallet listener
|
||||||
multisigWallet.addListener(depositTxListener);
|
havenoWallet.addListener(depositTxListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public MoneroTxWallet getTakerDepositTx() {
|
public MoneroTx getTakerDepositTx() {
|
||||||
String depositTxHash = getProcessModel().getTaker().getDepositTxHash();
|
String depositTxHash = getProcessModel().getTaker().getDepositTxHash();
|
||||||
try {
|
try {
|
||||||
if (takerDepositTx == null) takerDepositTx = depositTxHash == null ? null : xmrWalletService.getMultisigWallet(getId()).getTx(depositTxHash);
|
if (takerDepositTx == null) takerDepositTx = depositTxHash == null ? null : getXmrWalletService().getDaemon().getTx(depositTxHash);
|
||||||
return takerDepositTx;
|
return takerDepositTx;
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
log.error("Wallet is missing taker deposit tx " + depositTxHash);
|
log.error("Wallet is missing taker deposit tx " + depositTxHash);
|
||||||
@ -770,10 +802,10 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public MoneroTxWallet getMakerDepositTx() {
|
public MoneroTx getMakerDepositTx() {
|
||||||
String depositTxHash = getProcessModel().getMaker().getDepositTxHash();
|
String depositTxHash = getProcessModel().getMaker().getDepositTxHash();
|
||||||
try {
|
try {
|
||||||
if (makerDepositTx == null) makerDepositTx = depositTxHash == null ? null : xmrWalletService.getMultisigWallet(getId()).getTx(depositTxHash);
|
if (makerDepositTx == null) makerDepositTx = depositTxHash == null ? null : getXmrWalletService().getDaemon().getTx(depositTxHash);
|
||||||
return makerDepositTx;
|
return makerDepositTx;
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
log.error("Wallet is missing maker deposit tx " + depositTxHash);
|
log.error("Wallet is missing maker deposit tx " + depositTxHash);
|
||||||
@ -1047,10 +1079,10 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
private long getTradeStartTime() {
|
private long getTradeStartTime() {
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
long startTime;
|
long startTime;
|
||||||
final MoneroTxWallet takerDepositTx = getTakerDepositTx();
|
final MoneroTx takerDepositTx = getTakerDepositTx();
|
||||||
final MoneroTxWallet makerDepositTx = getMakerDepositTx();
|
final MoneroTx makerDepositTx = getMakerDepositTx();
|
||||||
if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) {
|
if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) {
|
||||||
if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) {
|
if (isDepositConfirmed()) {
|
||||||
final long tradeTime = getTakeOfferDate().getTime();
|
final long tradeTime = getTakeOfferDate().getTime();
|
||||||
long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight());
|
long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight());
|
||||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||||
|
@ -118,7 +118,7 @@ import javax.annotation.Nullable;
|
|||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.daemon.model.MoneroTx;
|
||||||
|
|
||||||
|
|
||||||
public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener {
|
public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener {
|
||||||
@ -346,7 +346,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
|
|
||||||
private void initPersistedTrade(Trade trade) {
|
private void initPersistedTrade(Trade trade) {
|
||||||
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
||||||
trade.updateDepositTxFromWallet(); // TODO (woodser): this re-opens all multisig wallets. only open active wallets
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -405,7 +404,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
Trade trade;
|
Trade trade;
|
||||||
Optional<Trade> tradeOptional = getTradeById(offer.getId());
|
Optional<Trade> tradeOptional = getOpenTrade(offer.getId());
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
trade = tradeOptional.get();
|
trade = tradeOptional.get();
|
||||||
|
|
||||||
@ -446,11 +445,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
initTradeAndProtocol(trade, getTradeProtocol(trade));
|
||||||
tradableList.add(trade);
|
synchronized (tradableList) {
|
||||||
|
tradableList.add(trade);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||||
log.warn("Arbitrator error during trade initialization: " + errorMessage);
|
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
||||||
removeTrade(trade);
|
removeTrade(trade);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -514,7 +515,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex());
|
trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex());
|
||||||
trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey());
|
trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey());
|
||||||
trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages());
|
trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages());
|
||||||
tradableList.add(trade);
|
synchronized (tradableList) {
|
||||||
|
tradableList.add(trade);
|
||||||
|
}
|
||||||
|
|
||||||
// notify on phase changes
|
// notify on phase changes
|
||||||
// TODO (woodser): save subscription, bind on startup
|
// TODO (woodser): save subscription, bind on startup
|
||||||
@ -545,7 +548,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + request.getTradeId());
|
log.warn("No trade with id " + request.getTradeId());
|
||||||
return;
|
return;
|
||||||
@ -564,7 +567,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + request.getTradeId());
|
log.warn("No trade with id " + request.getTradeId());
|
||||||
return;
|
return;
|
||||||
@ -583,7 +586,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + request.getTradeId());
|
log.warn("No trade with id " + request.getTradeId());
|
||||||
return;
|
return;
|
||||||
@ -602,7 +605,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + request.getTradeId());
|
log.warn("No trade with id " + request.getTradeId());
|
||||||
return;
|
return;
|
||||||
@ -621,7 +624,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(response.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(response.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + response.getTradeId());
|
log.warn("No trade with id " + response.getTradeId());
|
||||||
return;
|
return;
|
||||||
@ -640,7 +643,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) {
|
if (!tradeOptional.isPresent()) {
|
||||||
log.warn("No trade with id " + request.getTradeId());
|
log.warn("No trade with id " + request.getTradeId());
|
||||||
return;
|
return;
|
||||||
@ -659,7 +662,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
|
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||||
if (!tradeOptional.isPresent()) throw new RuntimeException("No trade with id " + request.getTradeId()); // TODO (woodser): error handling
|
if (!tradeOptional.isPresent()) throw new RuntimeException("No trade with id " + request.getTradeId()); // TODO (woodser): error handling
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
getTradeProtocol(trade).handleUpdateMultisigRequest(request, peer, errorMessage -> {
|
getTradeProtocol(trade).handleUpdateMultisigRequest(request, peer, errorMessage -> {
|
||||||
@ -737,7 +740,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
if (prev != null) {
|
if (prev != null) {
|
||||||
log.error("We had already an entry with uid {}", trade.getUid());
|
log.error("We had already an entry with uid {}", trade.getUid());
|
||||||
}
|
}
|
||||||
tradableList.add(trade);
|
synchronized (tradableList) {
|
||||||
|
tradableList.add(trade);
|
||||||
|
}
|
||||||
|
|
||||||
initTradeAndProtocol(trade, tradeProtocol);
|
initTradeAndProtocol(trade, tradeProtocol);
|
||||||
|
|
||||||
@ -753,7 +758,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
errorMessageHandler);
|
errorMessage -> {
|
||||||
|
log.warn("Taker error during check offer availability: " + errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
@ -797,8 +805,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
|
|
||||||
// If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades
|
// If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades
|
||||||
public void onTradeCompleted(Trade trade) {
|
public void onTradeCompleted(Trade trade) {
|
||||||
removeTrade(trade);
|
|
||||||
closedTradableManager.add(trade);
|
closedTradableManager.add(trade);
|
||||||
|
removeTrade(trade);
|
||||||
|
|
||||||
// TODO The address entry should have been removed already. Check and if its the case remove that.
|
// TODO The address entry should have been removed already. Check and if its the case remove that.
|
||||||
xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId());
|
xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId());
|
||||||
@ -811,7 +819,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) {
|
public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) {
|
||||||
Optional<Trade> tradeOptional = getTradeById(tradeId);
|
Optional<Trade> tradeOptional = getOpenTrade(tradeId);
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
Trade trade = tradeOptional.get();
|
Trade trade = tradeOptional.get();
|
||||||
trade.setDisputeState(disputeState);
|
trade.setDisputeState(disputeState);
|
||||||
@ -897,9 +905,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toSet()));
|
||||||
tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn()
|
tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn()
|
||||||
.map(trade -> {
|
.map(trade -> {
|
||||||
MoneroTxWallet makerDepositTx = trade.getMakerDepositTx();
|
MoneroTx makerDepositTx = trade.getMakerDepositTx();
|
||||||
if (makerDepositTx != null) {
|
if (makerDepositTx != null) {
|
||||||
if (makerDepositTx.isLocked()) {
|
if (!makerDepositTx.isConfirmed()) {
|
||||||
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. " +
|
||||||
@ -909,7 +917,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId())));
|
||||||
}
|
}
|
||||||
|
|
||||||
MoneroTxWallet takerDepositTx = trade.getTakerDepositTx();
|
MoneroTx takerDepositTx = trade.getTakerDepositTx();
|
||||||
if (takerDepositTx != null) {
|
if (takerDepositTx != null) {
|
||||||
if (!takerDepositTx.isConfirmed()) {
|
if (!takerDepositTx.isConfirmed()) {
|
||||||
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
|
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
|
||||||
@ -940,9 +948,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
|
|
||||||
initPersistedTrade(trade);
|
initPersistedTrade(trade);
|
||||||
|
|
||||||
if (!tradableList.contains(trade)) {
|
synchronized (tradableList) {
|
||||||
tradableList.add(trade);
|
if (!tradableList.contains(trade)) {
|
||||||
|
tradableList.add(trade);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -967,7 +978,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public ObservableList<Trade> getObservableList() {
|
public ObservableList<Trade> getObservableList() {
|
||||||
return tradableList.getObservableList();
|
synchronized (tradableList) {
|
||||||
|
return tradableList.getObservableList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanProperty persistedTradesInitializedProperty() {
|
public BooleanProperty persistedTradesInitializedProperty() {
|
||||||
@ -979,7 +992,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean wasOfferAlreadyUsedInTrade(String offerId) {
|
public boolean wasOfferAlreadyUsedInTrade(String offerId) {
|
||||||
return getTradeById(offerId).isPresent() ||
|
return getOpenTrade(offerId).isPresent() ||
|
||||||
failedTradesManager.getTradeById(offerId).isPresent() ||
|
failedTradesManager.getTradeById(offerId).isPresent() ||
|
||||||
closedTradableManager.getTradableById(offerId).isPresent();
|
closedTradableManager.getTradableById(offerId).isPresent();
|
||||||
}
|
}
|
||||||
@ -992,36 +1005,57 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
return offer.getDirection() == OfferPayload.Direction.SELL;
|
return offer.getDirection() == OfferPayload.Direction.SELL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<Trade> getTradeById(String tradeId) {
|
// TODO (woodser): make Optional<Trade> versus Trade return types consistent
|
||||||
|
public Trade getTrade(String tradeId) {
|
||||||
|
return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Trade> getOpenTrade(String tradeId) {
|
||||||
return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst();
|
return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Trade> getTrades() {
|
public List<Trade> getOpenTrades() {
|
||||||
return ImmutableList.copyOf(getObservableList().stream()
|
return ImmutableList.copyOf(getObservableList().stream()
|
||||||
.filter(e -> e instanceof Trade)
|
.filter(e -> e instanceof Trade)
|
||||||
.map(e -> e)
|
.map(e -> e)
|
||||||
.collect(Collectors.toList()));
|
.collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Trade> getClosedTrade(String tradeId) {
|
||||||
|
return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
private void removeTrade(Trade trade) {
|
private void removeTrade(Trade trade) {
|
||||||
if (tradableList.remove(trade)) {
|
log.info("TradeManager.removeTrade()");
|
||||||
|
synchronized(tradableList) {
|
||||||
|
if (!tradableList.contains(trade)) return;
|
||||||
|
|
||||||
// unreserve taker trade key images
|
synchronized (trade) {
|
||||||
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
|
|
||||||
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) {
|
// unreserve trade key images
|
||||||
xmrWalletService.getWallet().thawOutput(keyImage);
|
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
|
||||||
|
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) {
|
||||||
|
xmrWalletService.getWallet().thawOutput(keyImage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
// delete multisig wallet // TODO (woodser): don't delete multisig wallet until payout tx unlocked
|
||||||
xmrWalletService.deleteMultisigWallet(trade.getId()); // TODO (woodser): don't delete multisig wallet until payout tx unlocked?
|
if (xmrWalletService.multisigWalletExists(trade.getId())) xmrWalletService.deleteMultisigWallet(trade.getId());
|
||||||
requestPersistence();
|
else log.warn("Multisig wallet to delete for trade {} does not exist", trade.getId());
|
||||||
|
|
||||||
|
// unregister and persist
|
||||||
|
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
||||||
|
tradableList.remove(trade);
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addTrade(Trade trade) {
|
private void addTrade(Trade trade) {
|
||||||
if (tradableList.add(trade)) {
|
synchronized(tradableList) {
|
||||||
requestPersistence();
|
if (tradableList.add(trade)) {
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,9 +137,9 @@ public class TradeUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a transaction to reserve a trade. The deposit amount is returned
|
* Create a transaction to reserve a trade and freeze its funds. The deposit
|
||||||
* to the sender's payout address. Additional funds are reserved to allow
|
* amount is returned to the sender's payout address. Additional funds are
|
||||||
* fluctuations in the mining fee.
|
* reserved to allow fluctuations in the mining fee.
|
||||||
*
|
*
|
||||||
* @param xmrWalletService
|
* @param xmrWalletService
|
||||||
* @param offerId
|
* @param offerId
|
||||||
@ -147,7 +147,7 @@ public class TradeUtils {
|
|||||||
* @param depositAmount
|
* @param depositAmount
|
||||||
* @return a transaction to reserve a trade
|
* @return a transaction to reserve a trade
|
||||||
*/
|
*/
|
||||||
public static MoneroTxWallet createReserveTx(XmrWalletService xmrWalletService, String offerId, BigInteger tradeFee, String returnAddress, BigInteger depositAmount) {
|
public static MoneroTxWallet reserveTradeFunds(XmrWalletService xmrWalletService, String offerId, BigInteger tradeFee, String returnAddress, BigInteger depositAmount) {
|
||||||
|
|
||||||
// get expected mining fee
|
// get expected mining fee
|
||||||
MoneroWallet wallet = xmrWalletService.getWallet();
|
MoneroWallet wallet = xmrWalletService.getWallet();
|
||||||
@ -163,6 +163,11 @@ public class TradeUtils {
|
|||||||
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
|
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
|
||||||
.addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit?
|
.addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit?
|
||||||
|
|
||||||
|
// freeze trade funds
|
||||||
|
for (MoneroOutput input : reserveTx.getInputs()) {
|
||||||
|
wallet.freezeOutput(input.getKeyImage().getHex());
|
||||||
|
}
|
||||||
|
|
||||||
return reserveTx;
|
return reserveTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +227,9 @@ public class TradeUtils {
|
|||||||
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images");
|
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verify the unlock height
|
||||||
|
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
|
||||||
|
|
||||||
// verify trade fee
|
// verify trade fee
|
||||||
String feeAddress = TradeUtils.FEE_ADDRESS;
|
String feeAddress = TradeUtils.FEE_ADDRESS;
|
||||||
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
|
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
|
||||||
|
@ -8,14 +8,14 @@ import bisq.core.trade.messages.InitTradeRequest;
|
|||||||
import bisq.core.trade.messages.SignContractRequest;
|
import bisq.core.trade.messages.SignContractRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||||
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeAndMultisigRequests;
|
import bisq.core.trade.protocol.tasks.ArbitratorSendsInitTradeAndMultisigRequests;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessDepositRequest;
|
import bisq.core.trade.protocol.tasks.ArbitratorProcessesDepositRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
|
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ArbitratorProcessesReserveTx;
|
import bisq.core.trade.protocol.tasks.ArbitratorProcessesReserveTx;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessSignContractRequest;
|
import bisq.core.trade.protocol.tasks.ProcessSignContractRequest;
|
||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -34,94 +34,118 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
|||||||
public void handleInitTradeRequest(InitTradeRequest message,
|
public void handleInitTradeRequest(InitTradeRequest message,
|
||||||
NodeAddress peer,
|
NodeAddress peer,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
//processModel.setTempTradingPeerNodeAddress(peer);
|
processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set
|
||||||
expect(phase(Trade.Phase.INIT)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.with(message)
|
//processModel.setTempTradingPeerNodeAddress(peer);
|
||||||
.from(peer))
|
expect(phase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(message)
|
||||||
ApplyFilter.class,
|
.from(peer))
|
||||||
ProcessInitTradeRequest.class,
|
.setup(tasks(
|
||||||
ArbitratorProcessesReserveTx.class,
|
ApplyFilter.class,
|
||||||
ArbitratorSendsInitTradeAndMultisigRequests.class)
|
ProcessInitTradeRequest.class,
|
||||||
.using(new TradeTaskRunner(trade,
|
ArbitratorProcessesReserveTx.class,
|
||||||
() -> {
|
ArbitratorSendsInitTradeAndMultisigRequests.class)
|
||||||
handleTaskRunnerSuccess(peer, message);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(peer, message);
|
||||||
handleTaskRunnerFault(peer, message, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
handleTaskRunnerFault(peer, message, errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
System.out.println("ArbitratorProtocol.handleInitMultisigRequest()");
|
System.out.println("ArbitratorProtocol.handleInitMultisigRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(request);
|
||||||
.with(request)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(request)
|
||||||
ProcessInitMultisigRequest.class)
|
.from(sender))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
ProcessInitMultisigRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||||
System.out.println("ArbitratorProtocol.handleSignContractRequest()");
|
System.out.println("ArbitratorProtocol.handleSignContractRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
|
||||||
.with(message)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(message)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessSignContractRequest.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessSignContractRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, message);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
|
public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
|
||||||
System.out.println("ArbitratorProtocol.handleDepositRequest()");
|
System.out.println("ArbitratorProtocol.handleDepositRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(request);
|
||||||
.with(request)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(request)
|
||||||
ProcessDepositRequest.class)
|
.from(sender))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
ArbitratorProcessesDepositRequest.class)
|
||||||
stopTimeout();
|
.using(new TradeTaskRunner(trade,
|
||||||
handleTaskRunnerSuccess(sender, request);
|
() -> {
|
||||||
},
|
latch.countDown();
|
||||||
errorMessage -> {
|
stopTimeout();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -19,6 +19,7 @@ package bisq.core.trade.protocol;
|
|||||||
|
|
||||||
import bisq.core.trade.BuyerAsMakerTrade;
|
import bisq.core.trade.BuyerAsMakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
import bisq.core.trade.Trade.State;
|
||||||
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
|
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
import bisq.core.trade.messages.DepositResponse;
|
||||||
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
|
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
|
||||||
@ -47,11 +48,12 @@ import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment;
|
|||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol {
|
public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol {
|
||||||
@ -74,142 +76,191 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
|||||||
public void handleInitTradeRequest(InitTradeRequest message,
|
public void handleInitTradeRequest(InitTradeRequest message,
|
||||||
NodeAddress peer,
|
NodeAddress peer,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||||
expect(phase(Trade.Phase.INIT)
|
synchronized (trade) {
|
||||||
.with(message)
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
.from(peer))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(phase(Trade.Phase.INIT)
|
||||||
ProcessInitTradeRequest.class,
|
.with(message)
|
||||||
//ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here
|
.from(peer))
|
||||||
//VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee
|
.setup(tasks(
|
||||||
MakerSendsInitTradeRequestIfUnreserved.class)
|
ProcessInitTradeRequest.class,
|
||||||
.using(new TradeTaskRunner(trade,
|
//ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here
|
||||||
() -> {
|
//VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee
|
||||||
handleTaskRunnerSuccess(peer, message);
|
MakerSendsInitTradeRequestIfUnreserved.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(peer, message, errorMessage);
|
handleTaskRunnerSuccess(peer, message);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(peer, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleInitMultisigRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request); // TODO (woodser): synchronize access since concurrent requests processed
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(request);
|
||||||
.with(request)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(request)
|
||||||
ProcessInitMultisigRequest.class,
|
.from(sender))
|
||||||
SendSignContractRequestAfterMultisig.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
ProcessInitMultisigRequest.class,
|
||||||
() -> {
|
SendSignContractRequestAfterMultisig.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleSignContractRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(message);
|
||||||
.with(message)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(message)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessSignContractRequest.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessSignContractRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, message);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleSignContractResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
|
||||||
.with(message)
|
processModel.setTradeMessage(message);
|
||||||
.from(sender))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
// TODO (woodser): validate request
|
.with(message)
|
||||||
ProcessSignContractResponse.class)
|
.from(sender))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
// TODO (woodser): validate request
|
||||||
handleTaskRunnerSuccess(sender, message);
|
ProcessSignContractResponse.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleDepositResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(response);
|
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
processModel.setTradeMessage(response);
|
||||||
.with(response)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
.setup(tasks(
|
.with(response)
|
||||||
// TODO (woodser): validate request
|
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
||||||
ProcessDepositResponse.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessDepositResponse.class)
|
||||||
handleTaskRunnerSuccess(sender, response);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, response);
|
||||||
handleTaskRunnerFault(sender, response, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, response, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handlePaymentAccountPayloadRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
|
||||||
.with(request)
|
processModel.setTradeMessage(request);
|
||||||
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
|
||||||
// TODO (woodser): validate request
|
.with(request)
|
||||||
ProcessPaymentAccountPayloadRequest.class,
|
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
||||||
MakerRemovesOpenOffer.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessPaymentAccountPayloadRequest.class,
|
||||||
stopTimeout();
|
MakerRemovesOpenOffer.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
stopTimeout();
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -21,6 +21,7 @@ package bisq.core.trade.protocol;
|
|||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.trade.BuyerAsTakerTrade;
|
import bisq.core.trade.BuyerAsTakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
import bisq.core.trade.Trade.State;
|
||||||
import bisq.core.trade.handlers.TradeResultHandler;
|
import bisq.core.trade.handlers.TradeResultHandler;
|
||||||
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
|
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
import bisq.core.trade.messages.DepositResponse;
|
||||||
@ -32,6 +33,7 @@ import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
|||||||
import bisq.core.trade.messages.SignContractRequest;
|
import bisq.core.trade.messages.SignContractRequest;
|
||||||
import bisq.core.trade.messages.SignContractResponse;
|
import bisq.core.trade.messages.SignContractResponse;
|
||||||
import bisq.core.trade.messages.TradeMessage;
|
import bisq.core.trade.messages.TradeMessage;
|
||||||
|
import bisq.core.trade.protocol.TakerProtocol.TakerEvent;
|
||||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
|
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
|
||||||
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
|
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
|
||||||
@ -56,11 +58,12 @@ import bisq.core.trade.protocol.tasks.taker.TakerSendsInitTradeRequestToArbitrat
|
|||||||
import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment;
|
import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment;
|
||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@ -83,150 +86,198 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
|||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// User interaction: Take offer
|
// Take offer
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
// TODO (woodser): this implementation is duplicated with SellerAsTakerProtocol
|
|
||||||
@Override
|
|
||||||
public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) {
|
|
||||||
System.out.println("onTakeOffer()");
|
|
||||||
this.tradeResultHandler = tradeResultHandler;
|
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
|
||||||
expect(phase(Trade.Phase.INIT)
|
|
||||||
.with(TakerEvent.TAKE_OFFER)
|
|
||||||
.from(trade.getTradingPeerNodeAddress()))
|
|
||||||
.setup(tasks(
|
|
||||||
ApplyFilter.class,
|
|
||||||
TakerReservesTradeFunds.class,
|
|
||||||
TakerSendsInitTradeRequestToArbitrator.class) // TODO (woodser): app hangs if this pipeline fails. use .using() like below
|
|
||||||
.using(new TradeTaskRunner(trade,
|
|
||||||
() -> { },
|
|
||||||
errorMessageHandler))
|
|
||||||
.withTimeout(30))
|
|
||||||
.executeTasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// TakerProtocol
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// TODO (woodser): these methods are duplicated with SellerAsTakerProtocol due to single inheritance
|
// TODO (woodser): these methods are duplicated with SellerAsTakerProtocol due to single inheritance
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTakeOffer(TradeResultHandler tradeResultHandler,
|
||||||
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
|
System.out.println(getClass().getCanonicalName() + ".onTakeOffer()");
|
||||||
|
synchronized (trade) {
|
||||||
|
this.tradeResultHandler = tradeResultHandler;
|
||||||
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
expect(phase(Trade.Phase.INIT)
|
||||||
|
.with(TakerEvent.TAKE_OFFER)
|
||||||
|
.from(trade.getTradingPeerNodeAddress()))
|
||||||
|
.setup(tasks(
|
||||||
|
ApplyFilter.class,
|
||||||
|
TakerReservesTradeFunds.class,
|
||||||
|
TakerSendsInitTradeRequestToArbitrator.class)
|
||||||
|
.using(new TradeTaskRunner(trade,
|
||||||
|
() -> {
|
||||||
|
latch.countDown();
|
||||||
|
},
|
||||||
|
errorMessage -> {
|
||||||
|
latch.countDown();
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsTakerProtocol.handleInitMultisigRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(request);
|
||||||
.with(request)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(request)
|
||||||
ProcessInitMultisigRequest.class,
|
.from(sender))
|
||||||
SendSignContractRequestAfterMultisig.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
ProcessInitMultisigRequest.class,
|
||||||
() -> {
|
SendSignContractRequestAfterMultisig.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handleSignContractRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(message);
|
||||||
.with(message)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(message)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessSignContractRequest.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessSignContractRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, message);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handleSignContractResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
|
||||||
.with(message)
|
processModel.setTradeMessage(message);
|
||||||
.from(sender))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
// TODO (woodser): validate request
|
.with(message)
|
||||||
ProcessSignContractResponse.class)
|
.from(sender))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
// TODO (woodser): validate request
|
||||||
handleTaskRunnerSuccess(sender, message);
|
ProcessSignContractResponse.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state != State.CONTRACT_SIGNATURE_REQUESTED) return;
|
||||||
|
handleSignContractResponse(message, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handleDepositResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(response);
|
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
processModel.setTradeMessage(response);
|
||||||
.with(response)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
.setup(tasks(
|
.with(response)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessDepositResponse.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessDepositResponse.class)
|
||||||
handleTaskRunnerSuccess(sender, response);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, response);
|
||||||
handleTaskRunnerFault(sender, response, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, response, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handlePaymentAccountPayloadRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.with(request)
|
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
|
||||||
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
processModel.setTradeMessage(request);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) // TODO (woodser): rename to RECEIVED_DEPOSIT_TX_PUBLISHED_MSG
|
||||||
// TODO (woodser): validate request
|
.with(request)
|
||||||
ProcessPaymentAccountPayloadRequest.class)
|
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
// TODO (woodser): validate request
|
||||||
stopTimeout();
|
ProcessPaymentAccountPayloadRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
tradeResultHandler.handleResult(trade); // trade is initialized
|
() -> {
|
||||||
},
|
latch.countDown();
|
||||||
errorMessage -> {
|
stopTimeout();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
tradeResultHandler.handleResult(trade); // trade is initialized
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStar
|
|||||||
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener;
|
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener;
|
||||||
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
@ -127,27 +127,31 @@ public abstract class BuyerProtocol extends DisputeProtocol {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
|
System.out.println("BuyerProtocol.onPaymentStarted()");
|
||||||
expect(phase(Trade.Phase.DEPOSIT_CONFIRMED)
|
synchronized (trade) { // TODO (woodser): UpdateMultisigWithTradingPeer sends UpdateMultisigRequest and waits for UpdateMultisigResponse which is new thread, so synchronized (trade) in subsequent pipeline blocks forever if we hold on with countdown latch in this function
|
||||||
.with(event)
|
System.out.println("BuyerProtocol.onPaymentStarted() has the lock!!!");
|
||||||
.preCondition(trade.confirmPermitted()))
|
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
|
||||||
.setup(tasks(ApplyFilter.class,
|
expect(phase(Trade.Phase.DEPOSIT_CONFIRMED)
|
||||||
getVerifyPeersFeePaymentClass(),
|
.with(event)
|
||||||
UpdateMultisigWithTradingPeer.class,
|
.preCondition(trade.confirmPermitted()))
|
||||||
BuyerCreateAndSignPayoutTx.class,
|
.setup(tasks(ApplyFilter.class,
|
||||||
BuyerSetupPayoutTxListener.class,
|
getVerifyPeersFeePaymentClass(),
|
||||||
BuyerSendCounterCurrencyTransferStartedMessage.class)
|
UpdateMultisigWithTradingPeer.class,
|
||||||
.using(new TradeTaskRunner(trade,
|
BuyerCreateAndSignPayoutTx.class,
|
||||||
() -> {
|
BuyerSetupPayoutTxListener.class,
|
||||||
resultHandler.handleResult();
|
BuyerSendCounterCurrencyTransferStartedMessage.class)
|
||||||
handleTaskRunnerSuccess(event);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
(errorMessage) -> {
|
resultHandler.handleResult();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(event);
|
||||||
handleTaskRunnerFault(event, errorMessage);
|
},
|
||||||
})))
|
(errorMessage) -> {
|
||||||
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED))
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
.executeTasks();
|
handleTaskRunnerFault(event, errorMessage);
|
||||||
|
})))
|
||||||
|
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED))
|
||||||
|
.executeTasks();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -155,22 +159,29 @@ public abstract class BuyerProtocol extends DisputeProtocol {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
|
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
|
||||||
processModel.setTradeMessage(message);
|
log.info("BuyerProtocol.handle(PayoutTxPublishedMessage)");
|
||||||
processModel.setTempTradingPeerNodeAddress(peer);
|
synchronized (trade) {
|
||||||
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
|
processModel.setTradeMessage(message);
|
||||||
.with(message)
|
processModel.setTempTradingPeerNodeAddress(peer);
|
||||||
.from(peer))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
|
||||||
getVerifyPeersFeePaymentClass(),
|
.with(message)
|
||||||
BuyerProcessPayoutTxPublishedMessage.class)
|
.from(peer))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
getVerifyPeersFeePaymentClass(),
|
||||||
handleTaskRunnerSuccess(peer, message);
|
BuyerProcessPayoutTxPublishedMessage.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
handleTaskRunnerFault(peer, message, errorMessage);
|
latch.countDown();
|
||||||
})))
|
handleTaskRunnerSuccess(peer, message);
|
||||||
.executeTasks();
|
},
|
||||||
|
errorMessage -> {
|
||||||
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(peer, message, errorMessage);
|
||||||
|
})))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,26 +92,28 @@ public class FluentProtocol {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (setup.getTimeoutSec() > 0) {
|
synchronized (tradeProtocol.trade) {
|
||||||
tradeProtocol.startTimeout(setup.getTimeoutSec());
|
if (setup.getTimeoutSec() > 0) {
|
||||||
}
|
tradeProtocol.startTimeout(setup.getTimeoutSec());
|
||||||
|
}
|
||||||
|
|
||||||
NodeAddress peer = condition.getPeer();
|
NodeAddress peer = condition.getPeer();
|
||||||
if (peer != null) {
|
if (peer != null) {
|
||||||
tradeProtocol.processModel.setTempTradingPeerNodeAddress(peer); // TODO (woodser): node has multiple peers (arbitrator and maker or taker), but fluent protocol assumes only one
|
tradeProtocol.processModel.setTempTradingPeerNodeAddress(peer); // TODO (woodser): node has multiple peers (arbitrator and maker or taker), but fluent protocol assumes only one
|
||||||
tradeProtocol.processModel.getTradeManager().requestPersistence();
|
tradeProtocol.processModel.getTradeManager().requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeMessage message = condition.getMessage();
|
TradeMessage message = condition.getMessage();
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
tradeProtocol.processModel.setTradeMessage(message);
|
tradeProtocol.processModel.setTradeMessage(message);
|
||||||
tradeProtocol.processModel.getTradeManager().requestPersistence();
|
tradeProtocol.processModel.getTradeManager().requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeTaskRunner taskRunner = setup.getTaskRunner(peer, message, condition.getEvent());
|
TradeTaskRunner taskRunner = setup.getTaskRunner(peer, message, condition.getEvent());
|
||||||
taskRunner.addTasks(setup.getTasks());
|
taskRunner.addTasks(setup.getTasks());
|
||||||
taskRunner.run();
|
taskRunner.run();
|
||||||
return this;
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -181,14 +181,11 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private boolean multisigSetupComplete;
|
private String multisigAddress;
|
||||||
@Nullable
|
@Nullable
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private boolean makerReadyToFundMultisig; // TODO (woodser): remove
|
private boolean multisigSetupComplete; // TODO (woodser): redundant with multisigAddress existing, remove
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
private boolean multisigDepositInitiated;
|
|
||||||
@Nullable
|
@Nullable
|
||||||
transient private MoneroTxWallet buyerSignedPayoutTx; // TODO (woodser): remove
|
transient private MoneroTxWallet buyerSignedPayoutTx; // TODO (woodser): remove
|
||||||
|
|
||||||
@ -251,9 +248,8 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||||||
Optional.ofNullable(backupArbitrator).ifPresent(e -> builder.setBackupArbitrator(backupArbitrator.toProtoMessage()));
|
Optional.ofNullable(backupArbitrator).ifPresent(e -> builder.setBackupArbitrator(backupArbitrator.toProtoMessage()));
|
||||||
Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex));
|
Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex));
|
||||||
Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex));
|
Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex));
|
||||||
|
Optional.ofNullable(multisigAddress).ifPresent(e -> builder.setMultisigAddress(multisigAddress));
|
||||||
Optional.ofNullable(multisigSetupComplete).ifPresent(e -> builder.setMultisigSetupComplete(multisigSetupComplete));
|
Optional.ofNullable(multisigSetupComplete).ifPresent(e -> builder.setMultisigSetupComplete(multisigSetupComplete));
|
||||||
Optional.ofNullable(makerReadyToFundMultisig).ifPresent(e -> builder.setMakerReadyToFundMultisig(makerReadyToFundMultisig));
|
|
||||||
Optional.ofNullable(multisigDepositInitiated).ifPresent(e -> builder.setMultisigSetupComplete(multisigDepositInitiated));
|
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,9 +280,8 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||||||
processModel.setBackupArbitrator(proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
|
processModel.setBackupArbitrator(proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
|
||||||
processModel.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
|
processModel.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
|
||||||
processModel.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
|
processModel.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
|
||||||
|
processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress()));
|
||||||
processModel.setMultisigSetupComplete(proto.getMultisigSetupComplete());
|
processModel.setMultisigSetupComplete(proto.getMultisigSetupComplete());
|
||||||
processModel.setMakerReadyToFundMultisig(proto.getMakerReadyToFundMultisig());
|
|
||||||
processModel.setMultisigDepositInitiated(proto.getMultisigDepositInitiated());
|
|
||||||
|
|
||||||
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());
|
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());
|
||||||
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);
|
||||||
|
@ -20,6 +20,7 @@ package bisq.core.trade.protocol;
|
|||||||
|
|
||||||
import bisq.core.trade.SellerAsMakerTrade;
|
import bisq.core.trade.SellerAsMakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
import bisq.core.trade.Trade.State;
|
||||||
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
|
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
import bisq.core.trade.messages.DepositResponse;
|
||||||
import bisq.core.trade.messages.DepositTxMessage;
|
import bisq.core.trade.messages.DepositTxMessage;
|
||||||
@ -47,11 +48,12 @@ import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepo
|
|||||||
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage;
|
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerProcessDepositTxMessage;
|
||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtocol {
|
public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtocol {
|
||||||
@ -74,142 +76,191 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
|
|||||||
public void handleInitTradeRequest(InitTradeRequest message,
|
public void handleInitTradeRequest(InitTradeRequest message,
|
||||||
NodeAddress peer,
|
NodeAddress peer,
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||||
expect(phase(Trade.Phase.INIT)
|
synchronized (trade) {
|
||||||
.with(message)
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
.from(peer))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(phase(Trade.Phase.INIT)
|
||||||
ProcessInitTradeRequest.class,
|
.with(message)
|
||||||
//ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here
|
.from(peer))
|
||||||
//VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee
|
.setup(tasks(
|
||||||
MakerSendsInitTradeRequestIfUnreserved.class)
|
ProcessInitTradeRequest.class,
|
||||||
.using(new TradeTaskRunner(trade,
|
//ApplyFilter.class, // TODO (woodser): these checks apply when maker signs availability request, but not here
|
||||||
() -> {
|
//VerifyPeersAccountAgeWitness.class, // TODO (woodser): these checks apply after in multisig, means if rejected need to reimburse other's fee
|
||||||
handleTaskRunnerSuccess(peer, message);
|
MakerSendsInitTradeRequestIfUnreserved.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(peer, message, errorMessage);
|
handleTaskRunnerSuccess(peer, message);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(peer, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleInitMultisigRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request); // TODO (woodser): synchronize access since concurrent requests processed
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(request);
|
||||||
.with(request)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(request)
|
||||||
ProcessInitMultisigRequest.class,
|
.from(sender))
|
||||||
SendSignContractRequestAfterMultisig.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
ProcessInitMultisigRequest.class,
|
||||||
() -> {
|
SendSignContractRequestAfterMultisig.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleSignContractRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(message);
|
||||||
.with(message)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(message)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessSignContractRequest.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessSignContractRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, message);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleSignContractResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
|
||||||
.with(message)
|
processModel.setTradeMessage(message);
|
||||||
.from(sender))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
// TODO (woodser): validate request
|
.with(message)
|
||||||
ProcessSignContractResponse.class)
|
.from(sender))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
// TODO (woodser): validate request
|
||||||
handleTaskRunnerSuccess(sender, message);
|
ProcessSignContractResponse.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handleDepositResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(response);
|
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
processModel.setTradeMessage(response);
|
||||||
.with(response)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
.setup(tasks(
|
.with(response)
|
||||||
// TODO (woodser): validate request
|
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
||||||
ProcessDepositResponse.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessDepositResponse.class)
|
||||||
handleTaskRunnerSuccess(sender, response);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, response);
|
||||||
handleTaskRunnerFault(sender, response, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, response, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsMakerProtocol.handlePaymentAccountPayloadRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
|
||||||
.with(request)
|
processModel.setTradeMessage(request);
|
||||||
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
|
||||||
// TODO (woodser): validate request
|
.with(request)
|
||||||
ProcessPaymentAccountPayloadRequest.class,
|
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
||||||
MakerRemovesOpenOffer.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessPaymentAccountPayloadRequest.class,
|
||||||
stopTimeout();
|
MakerRemovesOpenOffer.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
stopTimeout();
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -21,6 +21,7 @@ package bisq.core.trade.protocol;
|
|||||||
import bisq.core.offer.Offer;
|
import bisq.core.offer.Offer;
|
||||||
import bisq.core.trade.SellerAsTakerTrade;
|
import bisq.core.trade.SellerAsTakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
import bisq.core.trade.Trade.State;
|
||||||
import bisq.core.trade.handlers.TradeResultHandler;
|
import bisq.core.trade.handlers.TradeResultHandler;
|
||||||
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
|
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
|
||||||
import bisq.core.trade.messages.DepositResponse;
|
import bisq.core.trade.messages.DepositResponse;
|
||||||
@ -51,11 +52,12 @@ import bisq.core.trade.protocol.tasks.taker.TakerVerifyMakerFeePayment;
|
|||||||
import bisq.core.util.Validator;
|
import bisq.core.util.Validator;
|
||||||
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@ -76,150 +78,198 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
|||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// User interaction: Take offer
|
// Take offer
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTakeOffer(TradeResultHandler tradeResultHandler,
|
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
|
||||||
System.out.println("onTakeOffer()");
|
|
||||||
this.tradeResultHandler = tradeResultHandler;
|
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
|
||||||
expect(phase(Trade.Phase.INIT)
|
|
||||||
.with(TakerEvent.TAKE_OFFER)
|
|
||||||
.from(trade.getTradingPeerNodeAddress()))
|
|
||||||
.setup(tasks(
|
|
||||||
ApplyFilter.class,
|
|
||||||
TakerReservesTradeFunds.class,
|
|
||||||
TakerSendsInitTradeRequestToArbitrator.class)
|
|
||||||
.using(new TradeTaskRunner(trade,
|
|
||||||
() -> { },
|
|
||||||
errorMessageHandler))
|
|
||||||
.withTimeout(30))
|
|
||||||
.executeTasks();
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// TakerProtocol
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// TODO (woodser): these methods are duplicated with BuyerAsTakerProtocol due to single inheritance
|
// TODO (woodser): these methods are duplicated with BuyerAsTakerProtocol due to single inheritance
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTakeOffer(TradeResultHandler tradeResultHandler,
|
||||||
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
|
System.out.println(getClass().getCanonicalName() + ".onTakeOffer()");
|
||||||
|
synchronized (trade) {
|
||||||
|
this.tradeResultHandler = tradeResultHandler;
|
||||||
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
expect(phase(Trade.Phase.INIT)
|
||||||
|
.with(TakerEvent.TAKE_OFFER)
|
||||||
|
.from(trade.getTradingPeerNodeAddress()))
|
||||||
|
.setup(tasks(
|
||||||
|
ApplyFilter.class,
|
||||||
|
TakerReservesTradeFunds.class,
|
||||||
|
TakerSendsInitTradeRequestToArbitrator.class)
|
||||||
|
.using(new TradeTaskRunner(trade,
|
||||||
|
() -> {
|
||||||
|
latch.countDown();
|
||||||
|
},
|
||||||
|
errorMessage -> {
|
||||||
|
latch.countDown();
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||||
System.out.println("BuyerAsTakerProtocol.handleInitMultisigRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(request);
|
||||||
.with(request)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(request)
|
||||||
ProcessInitMultisigRequest.class,
|
.from(sender))
|
||||||
SendSignContractRequestAfterMultisig.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
ProcessInitMultisigRequest.class,
|
||||||
() -> {
|
SendSignContractRequestAfterMultisig.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handleSignContractRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
processModel.setTradeMessage(message);
|
||||||
.with(message)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(anyPhase(Trade.Phase.INIT)
|
||||||
.setup(tasks(
|
.with(message)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessSignContractRequest.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessSignContractRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, message);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handleSignContractResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
expect(anyPhase(Trade.Phase.INIT)
|
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
|
||||||
.with(message)
|
processModel.setTradeMessage(message);
|
||||||
.from(sender))
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
// TODO (woodser): validate request
|
.with(message)
|
||||||
ProcessSignContractResponse.class)
|
.from(sender))
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
// TODO (woodser): validate request
|
||||||
handleTaskRunnerSuccess(sender, message);
|
ProcessSignContractResponse.class)
|
||||||
},
|
.using(new TradeTaskRunner(trade,
|
||||||
errorMessage -> {
|
() -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(sender, message, errorMessage);
|
handleTaskRunnerSuccess(sender, message);
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, message, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state != State.CONTRACT_SIGNATURE_REQUESTED) return;
|
||||||
|
handleSignContractResponse(message, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handleDepositResponse()");
|
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(response);
|
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
|
processModel.setTradeMessage(response);
|
||||||
.with(response)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(sender))
|
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
|
||||||
.setup(tasks(
|
.with(response)
|
||||||
// TODO (woodser): validate request
|
.from(sender))
|
||||||
ProcessDepositResponse.class)
|
.setup(tasks(
|
||||||
.using(new TradeTaskRunner(trade,
|
// TODO (woodser): validate request
|
||||||
() -> {
|
ProcessDepositResponse.class)
|
||||||
handleTaskRunnerSuccess(sender, response);
|
.using(new TradeTaskRunner(trade,
|
||||||
},
|
() -> {
|
||||||
errorMessage -> {
|
latch.countDown();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, response);
|
||||||
handleTaskRunnerFault(sender, response, errorMessage);
|
},
|
||||||
}))
|
errorMessage -> {
|
||||||
.withTimeout(30))
|
latch.countDown();
|
||||||
.executeTasks();
|
handleTaskRunnerFault(sender, response, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
|
||||||
System.out.println("SellerAsTakerProtocol.handlePaymentAccountPayloadRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) // TODO: only deposit_published should be expected
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.with(request)
|
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
|
||||||
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
processModel.setTradeMessage(request);
|
||||||
.setup(tasks(
|
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) // TODO (woodser): rename to RECEIVED_DEPOSIT_TX_PUBLISHED_MSG
|
||||||
// TODO (woodser): validate request
|
.with(request)
|
||||||
ProcessPaymentAccountPayloadRequest.class)
|
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
|
||||||
.using(new TradeTaskRunner(trade,
|
.setup(tasks(
|
||||||
() -> {
|
// TODO (woodser): validate request
|
||||||
stopTimeout();
|
ProcessPaymentAccountPayloadRequest.class)
|
||||||
handleTaskRunnerSuccess(sender, request);
|
.using(new TradeTaskRunner(trade,
|
||||||
tradeResultHandler.handleResult(trade); // trade is initialized
|
() -> {
|
||||||
},
|
latch.countDown();
|
||||||
errorMessage -> {
|
stopTimeout();
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
handleTaskRunnerSuccess(sender, request);
|
||||||
handleTaskRunnerFault(sender, request, errorMessage);
|
tradeResultHandler.handleResult(trade); // trade is initialized
|
||||||
}))
|
},
|
||||||
.withTimeout(30))
|
errorMessage -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
handleTaskRunnerFault(sender, request, errorMessage);
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
}))
|
||||||
|
.withTimeout(TRADE_TIMEOUT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
} else {
|
||||||
|
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||||
|
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
|
|||||||
import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx;
|
import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx;
|
||||||
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
@ -77,27 +77,39 @@ public abstract class SellerProtocol extends DisputeProtocol {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) {
|
protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) {
|
||||||
|
log.info("SellerProtocol.handle(CounterCurrencyTransferStartedMessage)");
|
||||||
// We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case
|
// We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case
|
||||||
// that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received
|
// that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received
|
||||||
// a mailbox message with CounterCurrencyTransferStartedMessage.
|
// a mailbox message with CounterCurrencyTransferStartedMessage.
|
||||||
// TODO A better fix would be to add a listener for the wallet sync state and process
|
// TODO A better fix would be to add a listener for the wallet sync state and process
|
||||||
// the mailbox msg once wallet is ready and trade state set.
|
// the mailbox msg once wallet is ready and trade state set.
|
||||||
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED)
|
synchronized (trade) {
|
||||||
.with(message)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.from(peer)
|
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED)
|
||||||
.preCondition(trade.getPayoutTx() == null,
|
.with(message)
|
||||||
() -> {
|
.from(peer)
|
||||||
log.warn("We received a CounterCurrencyTransferStartedMessage but we have already created the payout tx " +
|
.preCondition(trade.getPayoutTx() == null,
|
||||||
"so we ignore the message. This can happen if the ACK message to the peer did not " +
|
() -> {
|
||||||
"arrive and the peer repeats sending us the message. We send another ACK msg.");
|
log.warn("We received a CounterCurrencyTransferStartedMessage but we have already created the payout tx " +
|
||||||
sendAckMessage(peer, message, true, null);
|
"so we ignore the message. This can happen if the ACK message to the peer did not " +
|
||||||
removeMailboxMessageAfterProcessing(message);
|
"arrive and the peer repeats sending us the message. We send another ACK msg.");
|
||||||
}))
|
sendAckMessage(peer, message, true, null);
|
||||||
.setup(tasks(
|
removeMailboxMessageAfterProcessing(message);
|
||||||
SellerProcessCounterCurrencyTransferStartedMessage.class,
|
}))
|
||||||
ApplyFilter.class,
|
.setup(tasks(
|
||||||
getVerifyPeersFeePaymentClass()))
|
SellerProcessCounterCurrencyTransferStartedMessage.class,
|
||||||
.executeTasks();
|
ApplyFilter.class,
|
||||||
|
getVerifyPeersFeePaymentClass())
|
||||||
|
.using(new TradeTaskRunner(trade,
|
||||||
|
() -> {
|
||||||
|
latch.countDown();
|
||||||
|
},
|
||||||
|
(errorMessage) -> {
|
||||||
|
latch.countDown();
|
||||||
|
})))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -105,26 +117,33 @@ public abstract class SellerProtocol extends DisputeProtocol {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
|
log.info("SellerProtocol.onPaymentReceived()");
|
||||||
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
|
synchronized (trade) {
|
||||||
.with(event)
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
.preCondition(trade.confirmPermitted()))
|
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
|
||||||
.setup(tasks(
|
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
|
||||||
ApplyFilter.class,
|
.with(event)
|
||||||
getVerifyPeersFeePaymentClass(),
|
.preCondition(trade.confirmPermitted()))
|
||||||
SellerSignAndPublishPayoutTx.class,
|
.setup(tasks(
|
||||||
// SellerSignAndFinalizePayoutTx.class,
|
ApplyFilter.class,
|
||||||
// SellerBroadcastPayoutTx.class,
|
getVerifyPeersFeePaymentClass(),
|
||||||
SellerSendPayoutTxPublishedMessage.class)
|
SellerSignAndPublishPayoutTx.class,
|
||||||
.using(new TradeTaskRunner(trade, () -> {
|
// SellerSignAndFinalizePayoutTx.class,
|
||||||
resultHandler.handleResult();
|
// SellerBroadcastPayoutTx.class,
|
||||||
handleTaskRunnerSuccess(event);
|
SellerSendPayoutTxPublishedMessage.class)
|
||||||
}, (errorMessage) -> {
|
.using(new TradeTaskRunner(trade, () -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
latch.countDown();
|
||||||
handleTaskRunnerFault(event, errorMessage);
|
resultHandler.handleResult();
|
||||||
})))
|
handleTaskRunnerSuccess(event);
|
||||||
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT))
|
}, (errorMessage) -> {
|
||||||
.executeTasks();
|
latch.countDown();
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
handleTaskRunnerFault(event, errorMessage);
|
||||||
|
})))
|
||||||
|
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT))
|
||||||
|
.executeTasks();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ import bisq.common.taskrunner.Task;
|
|||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -58,6 +59,8 @@ import javax.annotation.Nullable;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener {
|
public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener {
|
||||||
|
|
||||||
|
public static final int TRADE_TIMEOUT = 60;
|
||||||
|
|
||||||
protected final ProcessModel processModel;
|
protected final ProcessModel processModel;
|
||||||
protected final Trade trade;
|
protected final Trade trade;
|
||||||
private Timer timeoutTimer;
|
private Timer timeoutTimer;
|
||||||
@ -213,23 +216,28 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
|
|
||||||
// TODO (woodser): update to use fluent for consistency
|
// TODO (woodser): update to use fluent for consistency
|
||||||
public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
|
public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
synchronized (trade) {
|
||||||
processModel.setTradeMessage(message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
|
processModel.setTradeMessage(message);
|
||||||
TradeTaskRunner taskRunner = new TradeTaskRunner(trade,
|
CountDownLatch latch = new CountDownLatch(1);
|
||||||
() -> {
|
TradeTaskRunner taskRunner = new TradeTaskRunner(trade,
|
||||||
stopTimeout();
|
() -> {
|
||||||
handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest");
|
stopTimeout();
|
||||||
},
|
latch.countDown();
|
||||||
errorMessage -> {
|
handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest");
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
},
|
||||||
handleTaskRunnerFault(peer, message, errorMessage);
|
errorMessage -> {
|
||||||
});
|
latch.countDown();
|
||||||
taskRunner.addTasks(
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
ProcessUpdateMultisigRequest.class
|
handleTaskRunnerFault(peer, message, errorMessage);
|
||||||
);
|
});
|
||||||
startTimeout(60); // TODO (woodser): what timeout to use? don't hardcode
|
taskRunner.addTasks(
|
||||||
taskRunner.run();
|
ProcessUpdateMultisigRequest.class
|
||||||
|
);
|
||||||
|
startTimeout(TRADE_TIMEOUT);
|
||||||
|
taskRunner.run();
|
||||||
|
wait(latch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -266,6 +274,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
return new FluentProtocol.Condition(trade).anyPhase(expectedPhases);
|
return new FluentProtocol.Condition(trade).anyPhase(expectedPhases);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected FluentProtocol.Condition state(Trade.State expectedState) {
|
||||||
|
return new FluentProtocol.Condition(trade).state(expectedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected FluentProtocol.Condition anyState(Trade.State... expectedStates) {
|
||||||
|
return new FluentProtocol.Condition(trade).anyState(expectedStates);
|
||||||
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
@SafeVarargs
|
||||||
public final FluentProtocol.Setup tasks(Class<? extends Task<Trade>>... tasks) {
|
public final FluentProtocol.Setup tasks(Class<? extends Task<Trade>>... tasks) {
|
||||||
return new FluentProtocol.Setup(this, trade).tasks(tasks);
|
return new FluentProtocol.Setup(this, trade).tasks(tasks);
|
||||||
@ -291,8 +307,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
log.info("Received AckMessage for {} from {} with tradeId {} and uid {}",
|
log.info("Received AckMessage for {} from {} with tradeId {} and uid {}",
|
||||||
ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getSourceUid());
|
ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getSourceUid());
|
||||||
} else {
|
} else {
|
||||||
log.warn("Received AckMessage with error state for {} from {} with tradeId {} and errorMessage={}",
|
String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() +
|
||||||
ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getErrorMessage());
|
" from "+ peer + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage();
|
||||||
|
log.warn(err);
|
||||||
|
stopTimeout();
|
||||||
|
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,6 +368,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
// Timeout
|
// Timeout
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
protected void wait(CountDownLatch latch) {
|
||||||
|
try {
|
||||||
|
latch.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void startTimeout(long timeoutSec) {
|
protected void startTimeout(long timeoutSec) {
|
||||||
stopTimeout();
|
stopTimeout();
|
||||||
timeoutTimer = UserThread.runAfter(() -> {
|
timeoutTimer = UserThread.runAfter(() -> {
|
||||||
|
@ -40,10 +40,10 @@ import monero.daemon.MoneroDaemon;
|
|||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ProcessDepositRequest extends TradeTask {
|
public class ArbitratorProcessesDepositRequest extends TradeTask {
|
||||||
|
|
||||||
@SuppressWarnings({"unused"})
|
@SuppressWarnings({"unused"})
|
||||||
public ProcessDepositRequest(TaskRunner taskHandler, Trade trade) {
|
public ArbitratorProcessesDepositRequest(TaskRunner taskHandler, Trade trade) {
|
||||||
super(taskHandler, trade);
|
super(taskHandler, trade);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,8 +78,7 @@ public class ProcessDepositRequest extends TradeTask {
|
|||||||
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress());
|
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress());
|
||||||
boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferPayload.Direction.SELL : offer.getDirection() == OfferPayload.Direction.BUY;
|
boolean isFromBuyer = isFromTaker ? offer.getDirection() == OfferPayload.Direction.SELL : offer.getDirection() == OfferPayload.Direction.BUY;
|
||||||
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
|
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
|
||||||
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId()); // TODO (woodser): only get, do not create
|
String depositAddress = processModel.getMultisigAddress();
|
||||||
String depositAddress = multisigWallet.getPrimaryAddress();
|
|
||||||
BigInteger tradeFee;
|
BigInteger tradeFee;
|
||||||
TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress());
|
TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress());
|
||||||
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
|
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
|
||||||
@ -103,34 +102,30 @@ public class ProcessDepositRequest extends TradeTask {
|
|||||||
null,
|
null,
|
||||||
false);
|
false);
|
||||||
|
|
||||||
// sychronize to send only one response
|
// set deposit info
|
||||||
synchronized(processModel) {
|
trader.setDepositTxHex(request.getDepositTxHex());
|
||||||
|
trader.setDepositTxKey(request.getDepositTxKey());
|
||||||
|
|
||||||
// set deposit info
|
// relay deposit txs when both available
|
||||||
trader.setDepositTxHex(request.getDepositTxHex());
|
// TODO (woodser): add small delay so tx has head start against double spend attempts?
|
||||||
trader.setDepositTxKey(request.getDepositTxKey());
|
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
||||||
|
|
||||||
// relay deposit txs when both available
|
// relay txs
|
||||||
// TODO (woodser): add small delay so tx has head start against double spend attempts?
|
daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); // TODO (woodser): check that result is good. will need to release funds if one is submitted
|
||||||
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
|
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
|
||||||
|
|
||||||
// relay txs
|
// create deposit response
|
||||||
daemon.submitTxHex(processModel.getMaker().getDepositTxHex());
|
DepositResponse response = new DepositResponse(
|
||||||
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
|
trade.getOffer().getId(),
|
||||||
|
processModel.getMyNodeAddress(),
|
||||||
|
processModel.getPubKeyRing(),
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
Version.getP2PMessageVersion(),
|
||||||
|
new Date().getTime());
|
||||||
|
|
||||||
// create deposit response
|
// send deposit response to maker and taker
|
||||||
DepositResponse response = new DepositResponse(
|
sendDepositResponse(trade.getMakerNodeAddress(), trade.getMakerPubKeyRing(), response);
|
||||||
trade.getOffer().getId(),
|
sendDepositResponse(trade.getTakerNodeAddress(), trade.getTakerPubKeyRing(), response);
|
||||||
processModel.getMyNodeAddress(),
|
|
||||||
processModel.getPubKeyRing(),
|
|
||||||
UUID.randomUUID().toString(),
|
|
||||||
Version.getP2PMessageVersion(),
|
|
||||||
new Date().getTime());
|
|
||||||
|
|
||||||
// send deposit response to maker and taker
|
|
||||||
sendDepositResponse(trade.getMakerNodeAddress(), trade.getMakerPubKeyRing(), response);
|
|
||||||
sendDepositResponse(trade.getTakerNodeAddress(), trade.getTakerPubKeyRing(), response);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (woodser): request persistence?
|
// TODO (woodser): request persistence?
|
@ -44,9 +44,6 @@ import monero.wallet.MoneroWallet;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
||||||
|
|
||||||
private boolean takerAck;
|
|
||||||
private boolean makerAck;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused"})
|
@SuppressWarnings({"unused"})
|
||||||
public ArbitratorSendsInitTradeAndMultisigRequests(TaskRunner taskHandler, Trade trade) {
|
public ArbitratorSendsInitTradeAndMultisigRequests(TaskRunner taskHandler, Trade trade) {
|
||||||
super(taskHandler, trade);
|
super(taskHandler, trade);
|
||||||
@ -97,14 +94,15 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
|||||||
null,
|
null,
|
||||||
null);
|
null);
|
||||||
|
|
||||||
// listen for maker to ack InitTradeRequest
|
// send init multisig requests on ack // TODO (woodser): only send InitMultisigRequests if arbitrator has maker reserve tx, else wait for that
|
||||||
TradeListener listener = new TradeListener() {
|
TradeListener listener = new TradeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
||||||
if (sender.equals(trade.getMakerNodeAddress()) && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) {
|
if (sender.equals(trade.getMakerNodeAddress()) &&
|
||||||
|
ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName()) &&
|
||||||
|
ackMessage.getSourceUid().equals(makerRequest.getUid())) {
|
||||||
trade.removeListener(this);
|
trade.removeListener(this);
|
||||||
if (ackMessage.isSuccess()) sendInitMultisigRequests();
|
if (ackMessage.isSuccess()) sendInitMultisigRequests();
|
||||||
else failed("Received unsuccessful ack for InitTradeRequest from maker"); // TODO (woodser): maker should not do this, penalize them by broadcasting reserve tx?
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -120,6 +118,7 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
|||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid());
|
log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid());
|
||||||
|
complete();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
@ -172,8 +171,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
|||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at arbitrator: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
|
log.info("{} arrived at arbitrator: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
|
||||||
makerAck = true;
|
|
||||||
checkComplete();
|
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
@ -194,8 +191,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
|||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at peer: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
|
log.info("{} arrived at peer: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
|
||||||
takerAck = true;
|
|
||||||
checkComplete();
|
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
@ -206,8 +201,4 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkComplete() {
|
|
||||||
if (makerAck && takerAck) complete();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -60,6 +60,7 @@ public class ProcessDepositResponse extends TradeTask {
|
|||||||
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
|
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
|
complete();
|
||||||
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
|
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
@ -69,8 +70,6 @@ public class ProcessDepositResponse extends TradeTask {
|
|||||||
failed();
|
failed();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
complete();
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
|
@ -52,8 +52,6 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
|||||||
|
|
||||||
private boolean ack1 = false;
|
private boolean ack1 = false;
|
||||||
private boolean ack2 = false;
|
private boolean ack2 = false;
|
||||||
private boolean failed = false;
|
|
||||||
private static Object lock = new Object();
|
|
||||||
MoneroWallet multisigWallet;
|
MoneroWallet multisigWallet;
|
||||||
|
|
||||||
@SuppressWarnings({"unused"})
|
@SuppressWarnings({"unused"})
|
||||||
@ -71,120 +69,119 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
|||||||
checkTradeId(processModel.getOfferId(), request);
|
checkTradeId(processModel.getOfferId(), request);
|
||||||
XmrWalletService xmrWalletService = processModel.getProvider().getXmrWalletService();
|
XmrWalletService xmrWalletService = processModel.getProvider().getXmrWalletService();
|
||||||
|
|
||||||
System.out.println("PROCESS MULTISIG MESSAGE");
|
|
||||||
System.out.println(request);
|
|
||||||
// System.out.println("PROCESS MULTISIG MESSAGE TRADE");
|
|
||||||
// System.out.println(trade);
|
|
||||||
|
|
||||||
// TODO (woodser): verify request including sender's signature in previous pipeline task
|
// TODO (woodser): verify request including sender's signature in previous pipeline task
|
||||||
// TODO (woodser): run in separate thread to not block UI thread?
|
// TODO (woodser): run in separate thread to not block UI thread?
|
||||||
// TODO (woodser): validate message has expected sender in previous step
|
// TODO (woodser): validate message has expected sender in previous step
|
||||||
|
|
||||||
// synchronize access to wallet
|
// get peer multisig participant
|
||||||
synchronized (lock) {
|
TradingPeer multisigParticipant;
|
||||||
|
if (request.getSenderNodeAddress().equals(trade.getMakerNodeAddress())) multisigParticipant = processModel.getMaker();
|
||||||
|
else if (request.getSenderNodeAddress().equals(trade.getTakerNodeAddress())) multisigParticipant = processModel.getTaker();
|
||||||
|
else if (request.getSenderNodeAddress().equals(trade.getArbitratorNodeAddress())) multisigParticipant = processModel.getArbitrator();
|
||||||
|
else throw new RuntimeException("Invalid sender to process init trade message: " + trade.getClass().getName());
|
||||||
|
|
||||||
// get peer multisig participant
|
// reconcile peer's established multisig hex with message
|
||||||
TradingPeer multisigParticipant;
|
if (multisigParticipant.getPreparedMultisigHex() == null) multisigParticipant.setPreparedMultisigHex(request.getPreparedMultisigHex());
|
||||||
if (request.getSenderNodeAddress().equals(trade.getMakerNodeAddress())) multisigParticipant = processModel.getMaker();
|
else if (!multisigParticipant.getPreparedMultisigHex().equals(request.getPreparedMultisigHex())) throw new RuntimeException("Message's prepared multisig differs from previous messages, previous: " + multisigParticipant.getPreparedMultisigHex() + ", message: " + request.getPreparedMultisigHex());
|
||||||
else if (request.getSenderNodeAddress().equals(trade.getTakerNodeAddress())) multisigParticipant = processModel.getTaker();
|
if (multisigParticipant.getMadeMultisigHex() == null) multisigParticipant.setMadeMultisigHex(request.getMadeMultisigHex());
|
||||||
else if (request.getSenderNodeAddress().equals(trade.getArbitratorNodeAddress())) multisigParticipant = processModel.getArbitrator();
|
else if (!multisigParticipant.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages: " + request.getMadeMultisigHex() + " versus " + multisigParticipant.getMadeMultisigHex());
|
||||||
else throw new RuntimeException("Invalid sender to process init trade message: " + trade.getClass().getName());
|
|
||||||
|
|
||||||
// reconcile peer's established multisig hex with message
|
// prepare multisig if applicable
|
||||||
if (multisigParticipant.getPreparedMultisigHex() == null) multisigParticipant.setPreparedMultisigHex(request.getPreparedMultisigHex());
|
boolean updateParticipants = false;
|
||||||
else if (!multisigParticipant.getPreparedMultisigHex().equals(request.getPreparedMultisigHex())) throw new RuntimeException("Message's prepared multisig differs from previous messages, previous: " + multisigParticipant.getPreparedMultisigHex() + ", message: " + request.getPreparedMultisigHex());
|
if (processModel.getPreparedMultisigHex() == null) {
|
||||||
if (multisigParticipant.getMadeMultisigHex() == null) multisigParticipant.setMadeMultisigHex(request.getMadeMultisigHex());
|
log.info("Preparing multisig wallet for trade {}", trade.getId());
|
||||||
else if (!multisigParticipant.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages");
|
multisigWallet = xmrWalletService.createMultisigWallet(trade.getId());
|
||||||
|
processModel.setPreparedMultisigHex(multisigWallet.prepareMultisig());
|
||||||
// prepare multisig if applicable
|
updateParticipants = true;
|
||||||
boolean updateParticipants = false;
|
} else if (!processModel.isMultisigSetupComplete()) {
|
||||||
if (processModel.getPreparedMultisigHex() == null) {
|
multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
||||||
System.out.println("Preparing multisig wallet!");
|
|
||||||
multisigWallet = xmrWalletService.createMultisigWallet(trade.getId());
|
|
||||||
processModel.setPreparedMultisigHex(multisigWallet.prepareMultisig());
|
|
||||||
updateParticipants = true;
|
|
||||||
} else {
|
|
||||||
multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
// make multisig if applicable
|
|
||||||
TradingPeer[] peers = getMultisigPeers();
|
|
||||||
if (processModel.getMadeMultisigHex() == null && peers[0].getPreparedMultisigHex() != null && peers[1].getPreparedMultisigHex() != null) {
|
|
||||||
System.out.println("Making multisig wallet!");
|
|
||||||
MoneroMultisigInitResult result = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, xmrWalletService.getWalletPassword()); // TODO (woodser): xmrWalletService.makeMultisig(tradeId, multisigHexes, threshold)?
|
|
||||||
processModel.setMadeMultisigHex(result.getMultisigHex());
|
|
||||||
updateParticipants = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// exchange multisig keys if applicable
|
|
||||||
if (!processModel.isMultisigSetupComplete() && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) {
|
|
||||||
System.out.println("Exchanging multisig wallet!");
|
|
||||||
multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), xmrWalletService.getWalletPassword());
|
|
||||||
processModel.setMultisigSetupComplete(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update multisig participants if new state to communicate
|
|
||||||
if (updateParticipants) {
|
|
||||||
|
|
||||||
// get destination addresses and pub key rings // TODO: better way, use getMultisigPeers()
|
|
||||||
NodeAddress peer1Address;
|
|
||||||
PubKeyRing peer1PubKeyRing;
|
|
||||||
NodeAddress peer2Address;
|
|
||||||
PubKeyRing peer2PubKeyRing;
|
|
||||||
if (trade instanceof ArbitratorTrade) {
|
|
||||||
peer1Address = trade.getTakerNodeAddress();
|
|
||||||
peer1PubKeyRing = trade.getTakerPubKeyRing();
|
|
||||||
peer2Address = trade.getMakerNodeAddress();
|
|
||||||
peer2PubKeyRing = trade.getMakerPubKeyRing();
|
|
||||||
} else if (trade instanceof MakerTrade) {
|
|
||||||
peer1Address = trade.getTakerNodeAddress();
|
|
||||||
peer1PubKeyRing = trade.getTakerPubKeyRing();
|
|
||||||
peer2Address = trade.getArbitratorNodeAddress();
|
|
||||||
peer2PubKeyRing = trade.getArbitratorPubKeyRing();
|
|
||||||
} else {
|
|
||||||
peer1Address = trade.getMakerNodeAddress();
|
|
||||||
peer1PubKeyRing = trade.getMakerPubKeyRing();
|
|
||||||
peer2Address = trade.getArbitratorNodeAddress();
|
|
||||||
peer2PubKeyRing = trade.getArbitratorPubKeyRing();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (peer1Address == null) throw new RuntimeException("Peer1 address is null");
|
|
||||||
if (peer1PubKeyRing == null) throw new RuntimeException("Peer1 pub key ring is null");
|
|
||||||
if (peer2Address == null) throw new RuntimeException("Peer2 address is null");
|
|
||||||
if (peer2PubKeyRing == null) throw new RuntimeException("Peer2 pub key ring null");
|
|
||||||
|
|
||||||
// complete on successful ack messages
|
|
||||||
TradeListener ackListener = new TradeListener() {
|
|
||||||
@Override
|
|
||||||
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
|
||||||
if (!ackMessage.getSourceMsgClassName().equals(InitMultisigRequest.class.getSimpleName())) return;
|
|
||||||
if (ackMessage.isSuccess()) {
|
|
||||||
if (sender.equals(peer1Address)) ack1 = true;
|
|
||||||
if (sender.equals(peer2Address)) ack2 = true;
|
|
||||||
if (ack1 && ack2) {
|
|
||||||
trade.removeListener(this);
|
|
||||||
completeAux();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!failed) {
|
|
||||||
failed = true;
|
|
||||||
failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
trade.addListener(ackListener);
|
|
||||||
|
|
||||||
// send to peers
|
|
||||||
sendInitMultisigRequest(peer1Address, peer1PubKeyRing);
|
|
||||||
sendInitMultisigRequest(peer2Address, peer2PubKeyRing);
|
|
||||||
} else {
|
|
||||||
completeAux();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Throwable t) {
|
|
||||||
failed(t);
|
// make multisig if applicable
|
||||||
}
|
TradingPeer[] peers = getMultisigPeers();
|
||||||
|
if (processModel.getMadeMultisigHex() == null && peers[0].getPreparedMultisigHex() != null && peers[1].getPreparedMultisigHex() != null) {
|
||||||
|
log.info("Making multisig wallet for trade {}", trade.getId());
|
||||||
|
MoneroMultisigInitResult result = multisigWallet.makeMultisig(Arrays.asList(peers[0].getPreparedMultisigHex(), peers[1].getPreparedMultisigHex()), 2, xmrWalletService.getWalletPassword()); // TODO (woodser): xmrWalletService.makeMultisig(tradeId, multisigHexes, threshold)?
|
||||||
|
processModel.setMadeMultisigHex(result.getMultisigHex());
|
||||||
|
updateParticipants = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// exchange multisig keys if applicable
|
||||||
|
if (!processModel.isMultisigSetupComplete() && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) {
|
||||||
|
log.info("Exchanging multisig wallet keys for trade {}", trade.getId());
|
||||||
|
multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getMadeMultisigHex(), peers[1].getMadeMultisigHex()), xmrWalletService.getWalletPassword());
|
||||||
|
processModel.setMultisigSetupComplete(true); // TODO: (woodser): remove this field?
|
||||||
|
processModel.setMultisigAddress(multisigWallet.getPrimaryAddress());
|
||||||
|
processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); // save and close multisig wallet once it's created
|
||||||
|
}
|
||||||
|
|
||||||
|
// update multisig participants if new state to communicate
|
||||||
|
if (updateParticipants) {
|
||||||
|
|
||||||
|
// get destination addresses and pub key rings // TODO: better way, use getMultisigPeers()
|
||||||
|
NodeAddress peer1Address;
|
||||||
|
PubKeyRing peer1PubKeyRing;
|
||||||
|
NodeAddress peer2Address;
|
||||||
|
PubKeyRing peer2PubKeyRing;
|
||||||
|
if (trade instanceof ArbitratorTrade) {
|
||||||
|
peer1Address = trade.getTakerNodeAddress();
|
||||||
|
peer1PubKeyRing = trade.getTakerPubKeyRing();
|
||||||
|
peer2Address = trade.getMakerNodeAddress();
|
||||||
|
peer2PubKeyRing = trade.getMakerPubKeyRing();
|
||||||
|
} else if (trade instanceof MakerTrade) {
|
||||||
|
peer1Address = trade.getTakerNodeAddress();
|
||||||
|
peer1PubKeyRing = trade.getTakerPubKeyRing();
|
||||||
|
peer2Address = trade.getArbitratorNodeAddress();
|
||||||
|
peer2PubKeyRing = trade.getArbitratorPubKeyRing();
|
||||||
|
} else {
|
||||||
|
peer1Address = trade.getMakerNodeAddress();
|
||||||
|
peer1PubKeyRing = trade.getMakerPubKeyRing();
|
||||||
|
peer2Address = trade.getArbitratorNodeAddress();
|
||||||
|
peer2PubKeyRing = trade.getArbitratorPubKeyRing();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (peer1Address == null) throw new RuntimeException("Peer1 address is null");
|
||||||
|
if (peer1PubKeyRing == null) throw new RuntimeException("Peer1 pub key ring is null");
|
||||||
|
if (peer2Address == null) throw new RuntimeException("Peer2 address is null");
|
||||||
|
if (peer2PubKeyRing == null) throw new RuntimeException("Peer2 pub key ring null");
|
||||||
|
|
||||||
|
// send to peer 1
|
||||||
|
sendInitMultisigRequest(peer1Address, peer1PubKeyRing, new SendDirectMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer1Address, request.getTradeId(), request.getUid());
|
||||||
|
ack1 = true;
|
||||||
|
if (ack1 && ack2) completeAux();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), peer1Address, errorMessage);
|
||||||
|
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
|
||||||
|
failed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// send to peer 2
|
||||||
|
sendInitMultisigRequest(peer2Address, peer2PubKeyRing, new SendDirectMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer2Address, request.getTradeId(), request.getUid());
|
||||||
|
ack2 = true;
|
||||||
|
if (ack1 && ack2) completeAux();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), peer2Address, errorMessage);
|
||||||
|
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
|
||||||
|
failed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
completeAux();
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
failed(t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TradingPeer[] getMultisigPeers() {
|
private TradingPeer[] getMultisigPeers() {
|
||||||
@ -202,9 +199,9 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
|||||||
return peers;
|
return peers;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendInitMultisigRequest(NodeAddress recipient, PubKeyRing pubKeyRing) {
|
private void sendInitMultisigRequest(NodeAddress recipient, PubKeyRing pubKeyRing, SendDirectMessageListener listener) {
|
||||||
|
|
||||||
// create request with current multisig hex
|
// create multisig message with current multisig hex
|
||||||
InitMultisigRequest request = new InitMultisigRequest(
|
InitMultisigRequest request = new InitMultisigRequest(
|
||||||
processModel.getOffer().getId(),
|
processModel.getOffer().getId(),
|
||||||
processModel.getMyNodeAddress(),
|
processModel.getMyNodeAddress(),
|
||||||
@ -216,22 +213,10 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
|||||||
processModel.getMadeMultisigHex());
|
processModel.getMadeMultisigHex());
|
||||||
|
|
||||||
log.info("Send {} with offerId {} and uid {} to peer {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), recipient);
|
log.info("Send {} with offerId {} and uid {} to peer {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), recipient);
|
||||||
processModel.getP2PService().sendEncryptedDirectMessage(recipient, pubKeyRing, request, new SendDirectMessageListener() {
|
processModel.getP2PService().sendEncryptedDirectMessage(recipient, pubKeyRing, request, listener);
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), recipient, request.getTradeId(), request.getUid());
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), request.getUid(), recipient, errorMessage);
|
|
||||||
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
|
|
||||||
failed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void completeAux() {
|
private void completeAux() {
|
||||||
multisigWallet.save();
|
|
||||||
complete();
|
complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,12 +61,6 @@ public class ProcessPaymentAccountPayloadRequest extends TradeTask {
|
|||||||
// set payment account payload
|
// set payment account payload
|
||||||
trade.getTradingPeer().setPaymentAccountPayload(paymentAccountPayload);
|
trade.getTradingPeer().setPaymentAccountPayload(paymentAccountPayload);
|
||||||
|
|
||||||
// subscribe to trade state to notify ui when deposit txs seen in network
|
|
||||||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> {
|
|
||||||
if (trade.isDepositPublished()) applyPublishedDepositTxs();
|
|
||||||
});
|
|
||||||
if (trade.isDepositPublished()) applyPublishedDepositTxs(); // deposit txs might be seen before subcription
|
|
||||||
|
|
||||||
// persist and complete
|
// persist and complete
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
@ -75,14 +69,6 @@ public class ProcessPaymentAccountPayloadRequest extends TradeTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyPublishedDepositTxs() {
|
|
||||||
MoneroWallet multisigWallet = processModel.getXmrWalletService().getMultisigWallet(trade.getId());
|
|
||||||
MoneroTxWallet makerDepositTx = checkNotNull(multisigWallet.getTx(processModel.getMaker().getDepositTxHash()));
|
|
||||||
MoneroTxWallet takerDepositTx = checkNotNull(multisigWallet.getTx(processModel.getTaker().getDepositTxHash()));
|
|
||||||
trade.applyDepositTxs(makerDepositTx, takerDepositTx);
|
|
||||||
UserThread.execute(this::unSubscribe); // remove trade state subscription at callback
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unSubscribe() {
|
private void unSubscribe() {
|
||||||
if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe();
|
if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
@ -26,12 +26,11 @@ import bisq.common.util.Utilities;
|
|||||||
import bisq.core.trade.ArbitratorTrade;
|
import bisq.core.trade.ArbitratorTrade;
|
||||||
import bisq.core.trade.Contract;
|
import bisq.core.trade.Contract;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
import bisq.core.trade.Trade.State;
|
||||||
import bisq.core.trade.TradeUtils;
|
import bisq.core.trade.TradeUtils;
|
||||||
import bisq.core.trade.messages.SignContractRequest;
|
import bisq.core.trade.messages.SignContractRequest;
|
||||||
import bisq.core.trade.messages.SignContractResponse;
|
import bisq.core.trade.messages.SignContractResponse;
|
||||||
import bisq.core.trade.protocol.TradeListener;
|
|
||||||
import bisq.core.trade.protocol.TradingPeer;
|
import bisq.core.trade.protocol.TradingPeer;
|
||||||
import bisq.network.p2p.AckMessage;
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
import bisq.network.p2p.SendDirectMessageListener;
|
import bisq.network.p2p.SendDirectMessageListener;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -43,7 +42,6 @@ public class ProcessSignContractRequest extends TradeTask {
|
|||||||
|
|
||||||
private boolean ack1 = false;
|
private boolean ack1 = false;
|
||||||
private boolean ack2 = false;
|
private boolean ack2 = false;
|
||||||
private boolean failed = false;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused"})
|
@SuppressWarnings({"unused"})
|
||||||
public ProcessSignContractRequest(TaskRunner taskHandler, Trade trade) {
|
public ProcessSignContractRequest(TaskRunner taskHandler, Trade trade) {
|
||||||
@ -82,66 +80,60 @@ public class ProcessSignContractRequest extends TradeTask {
|
|||||||
trade.setContractAsJson(contractAsJson);
|
trade.setContractAsJson(contractAsJson);
|
||||||
trade.getSelf().setContractSignature(signature);
|
trade.getSelf().setContractSignature(signature);
|
||||||
|
|
||||||
|
// create response with contract signature
|
||||||
|
SignContractResponse response = new SignContractResponse(
|
||||||
|
trade.getOffer().getId(),
|
||||||
|
processModel.getMyNodeAddress(),
|
||||||
|
processModel.getPubKeyRing(),
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
Version.getP2PMessageVersion(),
|
||||||
|
new Date().getTime(),
|
||||||
|
signature);
|
||||||
|
|
||||||
// get response recipients. only arbitrator sends response to both peers
|
// get response recipients. only arbitrator sends response to both peers
|
||||||
NodeAddress recipient1 = trade instanceof ArbitratorTrade ? trade.getMakerNodeAddress() : trade.getTradingPeerNodeAddress();
|
NodeAddress recipient1 = trade instanceof ArbitratorTrade ? trade.getMakerNodeAddress() : trade.getTradingPeerNodeAddress();
|
||||||
PubKeyRing recipient1PubKey = trade instanceof ArbitratorTrade ? trade.getMakerPubKeyRing() : trade.getTradingPeerPubKeyRing();
|
PubKeyRing recipient1PubKey = trade instanceof ArbitratorTrade ? trade.getMakerPubKeyRing() : trade.getTradingPeerPubKeyRing();
|
||||||
NodeAddress recipient2 = trade instanceof ArbitratorTrade ? trade.getTakerNodeAddress() : null;
|
NodeAddress recipient2 = trade instanceof ArbitratorTrade ? trade.getTakerNodeAddress() : null;
|
||||||
PubKeyRing recipient2PubKey = trade instanceof ArbitratorTrade ? trade.getTakerPubKeyRing() : null;
|
PubKeyRing recipient2PubKey = trade instanceof ArbitratorTrade ? trade.getTakerPubKeyRing() : null;
|
||||||
|
|
||||||
// complete on successful ack messages
|
// send response to recipient 1
|
||||||
TradeListener ackListener = new TradeListener() {
|
processModel.getP2PService().sendEncryptedDirectMessage(recipient1, recipient1PubKey, response, new SendDirectMessageListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
public void onArrived() {
|
||||||
if (!ackMessage.getSourceMsgClassName().equals(SignContractResponse.class.getSimpleName())) return;
|
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient1, trade.getId());
|
||||||
if (ackMessage.isSuccess()) {
|
ack1 = true;
|
||||||
if (sender.equals(trade.getTradingPeerNodeAddress())) ack1 = true;
|
if (ack1 && (recipient2 == null || ack2)) complete();
|
||||||
if (sender.equals(trade.getArbitratorNodeAddress())) ack2 = true;
|
|
||||||
if (trade instanceof ArbitratorTrade ? ack1 && ack2 : ack1) { // only arbitrator sends response to both peers
|
|
||||||
trade.removeListener(this);
|
|
||||||
complete();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!failed) {
|
|
||||||
failed = true;
|
|
||||||
failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
@Override
|
||||||
trade.addListener(ackListener);
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), recipient1, trade.getId(), errorMessage);
|
||||||
|
appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage);
|
||||||
|
failed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// send contract signature response(s)
|
// send response to recipient 2 if applicable
|
||||||
if (recipient1 != null) sendSignContractResponse(recipient1, recipient1PubKey, signature);
|
if (recipient2 != null) {
|
||||||
if (recipient2 != null) sendSignContractResponse(recipient2, recipient2PubKey, signature);
|
processModel.getP2PService().sendEncryptedDirectMessage(recipient2, recipient2PubKey, response, new SendDirectMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient2, trade.getId());
|
||||||
|
ack2 = true;
|
||||||
|
if (ack1 && ack2) complete();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), recipient2, trade.getId(), errorMessage);
|
||||||
|
appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage);
|
||||||
|
failed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// update trade state
|
||||||
|
trade.setState(State.CONTRACT_SIGNATURE_REQUESTED);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendSignContractResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, String contractSignature) {
|
|
||||||
|
|
||||||
// create response with contract signature
|
|
||||||
SignContractResponse response = new SignContractResponse(
|
|
||||||
trade.getOffer().getId(),
|
|
||||||
processModel.getMyNodeAddress(),
|
|
||||||
processModel.getPubKeyRing(),
|
|
||||||
UUID.randomUUID().toString(),
|
|
||||||
Version.getP2PMessageVersion(),
|
|
||||||
new Date().getTime(),
|
|
||||||
contractSignature);
|
|
||||||
|
|
||||||
// send request
|
|
||||||
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), nodeAddress, trade.getId());
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), nodeAddress, trade.getId(), errorMessage);
|
|
||||||
appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage);
|
|
||||||
failed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,6 @@ import bisq.core.trade.messages.DepositRequest;
|
|||||||
import bisq.core.trade.messages.SignContractResponse;
|
import bisq.core.trade.messages.SignContractResponse;
|
||||||
import bisq.core.trade.protocol.TradingPeer;
|
import bisq.core.trade.protocol.TradingPeer;
|
||||||
import bisq.network.p2p.SendDirectMessageListener;
|
import bisq.network.p2p.SendDirectMessageListener;
|
||||||
import common.utils.GenUtils;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -45,12 +44,6 @@ public class ProcessSignContractResponse extends TradeTask {
|
|||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// wait until contract is available from peer's sign contract request
|
|
||||||
// TODO (woodser): this will loop if peer disappears; use proper notification
|
|
||||||
while (trade.getContract() == null) {
|
|
||||||
GenUtils.waitFor(250);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get contract and signature
|
// get contract and signature
|
||||||
String contractAsJson = trade.getContractAsJson();
|
String contractAsJson = trade.getContractAsJson();
|
||||||
SignContractResponse response = (SignContractResponse) processModel.getTradeMessage(); // TODO (woodser): verify response
|
SignContractResponse response = (SignContractResponse) processModel.getTradeMessage(); // TODO (woodser): verify response
|
||||||
@ -76,7 +69,7 @@ public class ProcessSignContractResponse extends TradeTask {
|
|||||||
if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
|
if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
|
||||||
|
|
||||||
// start listening for deposit txs
|
// start listening for deposit txs
|
||||||
trade.setupDepositTxsListener();
|
trade.listenForDepositTxs();
|
||||||
|
|
||||||
// create request for arbitrator to deposit funds to multisig
|
// create request for arbitrator to deposit funds to multisig
|
||||||
DepositRequest request = new DepositRequest(
|
DepositRequest request = new DepositRequest(
|
||||||
@ -95,6 +88,8 @@ public class ProcessSignContractResponse extends TradeTask {
|
|||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId());
|
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId());
|
||||||
|
processModel.getTradeManager().requestPersistence();
|
||||||
|
complete();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) {
|
public void onFault(String errorMessage) {
|
||||||
@ -103,11 +98,9 @@ public class ProcessSignContractResponse extends TradeTask {
|
|||||||
failed();
|
failed();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
complete(); // does not yet have needed signatures
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist and complete
|
|
||||||
processModel.getTradeManager().requestPersistence();
|
|
||||||
complete();
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,9 @@ public class ProcessUpdateMultisigRequest extends TradeTask {
|
|||||||
int numOutputsSigned = multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
|
int numOutputsSigned = multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
|
||||||
System.out.println("Num outputs signed by imported multisig hex: " + numOutputsSigned);
|
System.out.println("Num outputs signed by imported multisig hex: " + numOutputsSigned);
|
||||||
|
|
||||||
|
// close multisig wallet
|
||||||
|
processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId());
|
||||||
|
|
||||||
// respond with updated multisig hex
|
// respond with updated multisig hex
|
||||||
UpdateMultisigResponse response = new UpdateMultisigResponse(
|
UpdateMultisigResponse response = new UpdateMultisigResponse(
|
||||||
processModel.getOffer().getId(),
|
processModel.getOffer().getId(),
|
||||||
@ -90,9 +93,6 @@ public class ProcessUpdateMultisigRequest extends TradeTask {
|
|||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at trading peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid());
|
log.info("{} arrived at trading peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid());
|
||||||
|
|
||||||
// save multisig wallet
|
|
||||||
multisigWallet.save(); // TODO (woodser): save on each step or after multisig wallets created?
|
|
||||||
complete();
|
complete();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
|
@ -44,9 +44,8 @@ import monero.wallet.model.MoneroTxWallet;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SendSignContractRequestAfterMultisig extends TradeTask {
|
public class SendSignContractRequestAfterMultisig extends TradeTask {
|
||||||
|
|
||||||
private boolean ack1 = false;
|
private boolean ack1 = false; // TODO (woodser) these represent onArrived(), not the ack
|
||||||
private boolean ack2 = false;
|
private boolean ack2 = false;
|
||||||
private boolean failed = false;
|
|
||||||
|
|
||||||
@SuppressWarnings({"unused"})
|
@SuppressWarnings({"unused"})
|
||||||
public SendSignContractRequestAfterMultisig(TaskRunner taskHandler, Trade trade) {
|
public SendSignContractRequestAfterMultisig(TaskRunner taskHandler, Trade trade) {
|
||||||
@ -58,96 +57,93 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
|
|||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// skip if multisig wallet not complete
|
synchronized (trade.getXmrWalletService().getWallet()) { // synchronize on wallet to create deposit tx and freeze funds
|
||||||
if (!processModel.isMultisigSetupComplete()) return; // TODO: woodser: this does not ack original request?
|
|
||||||
|
|
||||||
// skip if deposit tx already created
|
// skip if multisig wallet not complete
|
||||||
if (processModel.getDepositTxXmr() != null) return;
|
if (!processModel.isMultisigSetupComplete()) {
|
||||||
|
complete();
|
||||||
// thaw reserved outputs
|
return; // TODO: woodser: this does not ack original request?
|
||||||
MoneroWallet wallet = trade.getXmrWalletService().getWallet();
|
|
||||||
for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) {
|
|
||||||
wallet.thawOutput(reserveTxKeyImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create deposit tx
|
|
||||||
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee());
|
|
||||||
Offer offer = processModel.getOffer();
|
|
||||||
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit());
|
|
||||||
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId());
|
|
||||||
String multisigAddress = multisigWallet.getPrimaryAddress();
|
|
||||||
MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount);
|
|
||||||
|
|
||||||
// freeze deposit outputs
|
|
||||||
// TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig
|
|
||||||
for (MoneroOutput input : depositTx.getInputs()) {
|
|
||||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
|
||||||
}
|
|
||||||
|
|
||||||
// save process state
|
|
||||||
processModel.setDepositTxXmr(depositTx);
|
|
||||||
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
|
||||||
|
|
||||||
// complete on successful ack messages
|
|
||||||
TradeListener ackListener = new TradeListener() {
|
|
||||||
@Override
|
|
||||||
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
|
|
||||||
if (!ackMessage.getSourceMsgClassName().equals(SignContractRequest.class.getSimpleName())) return;
|
|
||||||
if (ackMessage.isSuccess()) {
|
|
||||||
if (sender.equals(trade.getTradingPeerNodeAddress())) ack1 = true;
|
|
||||||
if (sender.equals(trade.getArbitratorNodeAddress())) ack2 = true;
|
|
||||||
if (ack1 && ack2) {
|
|
||||||
trade.removeListener(this);
|
|
||||||
completeAux();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!failed) {
|
|
||||||
failed = true;
|
|
||||||
failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
trade.addListener(ackListener);
|
|
||||||
|
|
||||||
// send sign contract requests to peer and arbitrator
|
// skip if deposit tx already created
|
||||||
sendSignContractRequest(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), offer, depositTx);
|
if (processModel.getDepositTxXmr() != null) {
|
||||||
sendSignContractRequest(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), offer, depositTx);
|
complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// thaw reserved outputs
|
||||||
|
MoneroWallet wallet = trade.getXmrWalletService().getWallet();
|
||||||
|
for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) {
|
||||||
|
wallet.thawOutput(reserveTxKeyImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create deposit tx
|
||||||
|
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee());
|
||||||
|
Offer offer = processModel.getOffer();
|
||||||
|
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit());
|
||||||
|
String multisigAddress = processModel.getMultisigAddress();
|
||||||
|
MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount);
|
||||||
|
|
||||||
|
// freeze deposit outputs
|
||||||
|
// TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig
|
||||||
|
for (MoneroOutput input : depositTx.getInputs()) {
|
||||||
|
wallet.freezeOutput(input.getKeyImage().getHex());
|
||||||
|
}
|
||||||
|
|
||||||
|
// save process state
|
||||||
|
processModel.setDepositTxXmr(depositTx);
|
||||||
|
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
||||||
|
|
||||||
|
// create request for peer and arbitrator to sign contract
|
||||||
|
SignContractRequest request = new SignContractRequest(
|
||||||
|
trade.getOffer().getId(),
|
||||||
|
processModel.getMyNodeAddress(),
|
||||||
|
processModel.getPubKeyRing(),
|
||||||
|
UUID.randomUUID().toString(),
|
||||||
|
Version.getP2PMessageVersion(),
|
||||||
|
new Date().getTime(),
|
||||||
|
trade.getProcessModel().getAccountId(),
|
||||||
|
trade.getProcessModel().getPaymentAccountPayload(trade).getHash(),
|
||||||
|
trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
||||||
|
depositTx.getHash());
|
||||||
|
|
||||||
|
// send request to trading peer
|
||||||
|
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
|
||||||
|
ack1 = true;
|
||||||
|
if (ack1 && ack2) completeAux();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId(), errorMessage);
|
||||||
|
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
|
||||||
|
failed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// send request to arbitrator
|
||||||
|
processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() {
|
||||||
|
@Override
|
||||||
|
public void onArrived() {
|
||||||
|
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId());
|
||||||
|
ack2 = true;
|
||||||
|
if (ack1 && ack2) completeAux();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFault(String errorMessage) {
|
||||||
|
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId(), errorMessage);
|
||||||
|
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
|
||||||
|
failed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendSignContractRequest(NodeAddress nodeAddress, PubKeyRing pubKeyRing, Offer offer, MoneroTxWallet depositTx) {
|
|
||||||
|
|
||||||
// create request to sign contract
|
|
||||||
SignContractRequest request = new SignContractRequest(
|
|
||||||
trade.getOffer().getId(),
|
|
||||||
processModel.getMyNodeAddress(),
|
|
||||||
processModel.getPubKeyRing(),
|
|
||||||
UUID.randomUUID().toString(), // TODO: ensure not reusing request id across protocol
|
|
||||||
Version.getP2PMessageVersion(),
|
|
||||||
new Date().getTime(),
|
|
||||||
trade.getProcessModel().getAccountId(),
|
|
||||||
trade.getProcessModel().getPaymentAccountPayload(trade).getHash(),
|
|
||||||
trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
|
||||||
depositTx.getHash());
|
|
||||||
|
|
||||||
// send request
|
|
||||||
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, request, new SendDirectMessageListener() {
|
|
||||||
@Override
|
|
||||||
public void onArrived() {
|
|
||||||
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), nodeAddress, trade.getId());
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onFault(String errorMessage) {
|
|
||||||
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), nodeAddress, trade.getId(), errorMessage);
|
|
||||||
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
|
|
||||||
failed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void completeAux() {
|
private void completeAux() {
|
||||||
processModel.getXmrWalletService().getWallet().save();
|
processModel.getXmrWalletService().getWallet().save();
|
||||||
complete();
|
complete();
|
||||||
|
@ -33,7 +33,7 @@ public class SetupDepositTxsListener extends TradeTask {
|
|||||||
protected void run() {
|
protected void run() {
|
||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
trade.setupDepositTxsListener();
|
trade.listenForDepositTxs();
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
|
@ -57,7 +57,7 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
|
|||||||
|
|
||||||
// fetch relevant trade info
|
// fetch relevant trade info
|
||||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
|
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerCreateAndSignPayoutTx
|
||||||
|
|
||||||
// skip if multisig wallet does not need updated
|
// skip if multisig wallet does not need updated
|
||||||
if (!multisigWallet.isMultisigImportNeeded()) {
|
if (!multisigWallet.isMultisigImportNeeded()) {
|
||||||
@ -74,7 +74,6 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
|
|||||||
UpdateMultisigResponse response = (UpdateMultisigResponse) message;
|
UpdateMultisigResponse response = (UpdateMultisigResponse) message;
|
||||||
multisigWallet.importMultisigHex(Arrays.asList(response.getUpdatedMultisigHex()));
|
multisigWallet.importMultisigHex(Arrays.asList(response.getUpdatedMultisigHex()));
|
||||||
multisigWallet.sync();
|
multisigWallet.sync();
|
||||||
multisigWallet.save();
|
|
||||||
trade.removeListener(updateMultisigResponseListener);
|
trade.removeListener(updateMultisigResponseListener);
|
||||||
complete();
|
complete();
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
package bisq.core.trade.protocol.tasks.buyer;
|
package bisq.core.trade.protocol.tasks.buyer;
|
||||||
|
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.offer.Offer;
|
|
||||||
import bisq.core.trade.MakerTrade;
|
import bisq.core.trade.MakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||||
@ -42,10 +41,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
import monero.common.MoneroError;
|
import monero.common.MoneroError;
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroAccount;
|
import monero.wallet.model.MoneroAccount;
|
||||||
import monero.wallet.model.MoneroDestination;
|
|
||||||
import monero.wallet.model.MoneroSubaddress;
|
import monero.wallet.model.MoneroSubaddress;
|
||||||
import monero.wallet.model.MoneroTxConfig;
|
import monero.wallet.model.MoneroTxConfig;
|
||||||
import monero.wallet.model.MoneroTxQuery;
|
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -65,7 +62,7 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask {
|
|||||||
Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
|
Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
|
||||||
Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null");
|
Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null");
|
||||||
Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
|
Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
|
||||||
Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
|
checkNotNull(trade.getOffer(), "offer must not be null");
|
||||||
|
|
||||||
// gather relevant trade info
|
// gather relevant trade info
|
||||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||||
@ -84,8 +81,8 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask {
|
|||||||
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Multisig import is still needed!!!");
|
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Multisig import is still needed!!!");
|
||||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))) // reduce payment amount to compute fee of similar tx
|
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx
|
||||||
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5)))
|
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)))
|
||||||
.setRelay(false)
|
.setRelay(false)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -98,19 +95,18 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask {
|
|||||||
numAttempts++;
|
numAttempts++;
|
||||||
payoutTx = multisigWallet.createTx(new MoneroTxConfig()
|
payoutTx = multisigWallet.createTx(new MoneroTxConfig()
|
||||||
.setAccountIndex(0)
|
.setAccountIndex(0)
|
||||||
.addDestination(new MoneroDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))) // split fee subtracted from each payout amount
|
.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount
|
||||||
.addDestination(new MoneroDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))) // TODO (woodser): support addDestination(addr, amt) without new
|
.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))
|
||||||
.setRelay(false));
|
.setRelay(false));
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
//e.printStackTrace();
|
// exception expected
|
||||||
//System.out.println("FAILED TO CREATE PAYOUT TX, ITERATING...");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx");
|
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
|
||||||
System.out.println("PAYOUT TX GENERATED ON ATTEMPT " + numAttempts);
|
log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
||||||
System.out.println(payoutTx);
|
|
||||||
processModel.setBuyerSignedPayoutTx(payoutTx);
|
processModel.setBuyerSignedPayoutTx(payoutTx);
|
||||||
|
walletService.closeMultisigWallet(trade.getId());
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
|
@ -63,6 +63,7 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask {
|
|||||||
trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0)));
|
trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0)));
|
||||||
XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx());
|
XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx());
|
||||||
trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG);
|
trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG);
|
||||||
|
walletService.closeMultisigWallet(trade.getId());
|
||||||
//processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId());
|
//processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId());
|
||||||
} else {
|
} else {
|
||||||
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId());
|
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId());
|
||||||
|
@ -18,7 +18,6 @@
|
|||||||
package bisq.core.trade.protocol.tasks.seller;
|
package bisq.core.trade.protocol.tasks.seller;
|
||||||
|
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.offer.Offer;
|
|
||||||
import bisq.core.trade.Contract;
|
import bisq.core.trade.Contract;
|
||||||
import bisq.core.trade.MakerTrade;
|
import bisq.core.trade.MakerTrade;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
@ -31,10 +30,6 @@ import java.math.BigInteger;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroDestination;
|
import monero.wallet.model.MoneroDestination;
|
||||||
import monero.wallet.model.MoneroMultisigSignResult;
|
import monero.wallet.model.MoneroMultisigSignResult;
|
||||||
@ -59,29 +54,17 @@ public class SellerSignAndPublishPayoutTx extends TradeTask {
|
|||||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
|
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
|
||||||
String buyerSignedPayoutTxHex = trade.getTradingPeer().getSignedPayoutTxHex();
|
String buyerSignedPayoutTxHex = trade.getTradingPeer().getSignedPayoutTxHex();
|
||||||
Contract contract = trade.getContract();
|
Contract contract = trade.getContract();
|
||||||
Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
|
|
||||||
BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs trade.getDepositTxId() necessary or avoidable?
|
BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs trade.getDepositTxId() necessary or avoidable?
|
||||||
BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount();
|
BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount();
|
||||||
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount());
|
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount());
|
||||||
|
|
||||||
System.out.println("SELLER VERIFYING PAYOUT TX");
|
|
||||||
System.out.println("Trade amount: " + trade.getTradeAmount());
|
|
||||||
System.out.println("Buyer deposit amount: " + buyerDepositAmount);
|
|
||||||
System.out.println("Seller deposit amount: " + sellerDepositAmount);
|
|
||||||
|
|
||||||
BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(offer.getBuyerSecurityDeposit().add(trade.getTradeAmount()));
|
|
||||||
System.out.println("Buyer payout amount (with multiplier): " + buyerPayoutAmount);
|
|
||||||
BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(offer.getSellerSecurityDeposit());
|
|
||||||
System.out.println("Seller payout amount (with multiplier): " + sellerPayoutAmount);
|
|
||||||
|
|
||||||
// parse buyer-signed payout tx
|
// parse buyer-signed payout tx
|
||||||
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(buyerSignedPayoutTxHex));
|
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(buyerSignedPayoutTxHex));
|
||||||
if (parsedTxSet.getTxs().get(0).getTxSet() != parsedTxSet) System.out.println("LINKS ARE WRONG STRAIGHT FROM PARSING!!!");
|
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): test nack
|
||||||
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): nack
|
|
||||||
MoneroTxWallet buyerSignedPayoutTx = parsedTxSet.getTxs().get(0);
|
MoneroTxWallet buyerSignedPayoutTx = parsedTxSet.getTxs().get(0);
|
||||||
System.out.println("Parsed buyer signed tx hex:\n" + buyerSignedPayoutTx);
|
|
||||||
|
|
||||||
// verify payout tx has exactly 2 destinations
|
// verify payout tx has exactly 2 destinations
|
||||||
|
log.info("Seller verifying buyer-signed payout tx");
|
||||||
if (buyerSignedPayoutTx.getOutgoingTransfer() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Buyer-signed payout tx does not have exactly two destinations");
|
if (buyerSignedPayoutTx.getOutgoingTransfer() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Buyer-signed payout tx does not have exactly two destinations");
|
||||||
|
|
||||||
// get buyer and seller destinations (order not preserved)
|
// get buyer and seller destinations (order not preserved)
|
||||||
@ -93,8 +76,8 @@ public class SellerSignAndPublishPayoutTx extends TradeTask {
|
|||||||
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
|
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
|
||||||
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
||||||
|
|
||||||
// verify change address is multisig's primary address // TODO (woodser): ideally change amount is 0, seen with 0 conf payout tx
|
// verify change address is multisig's primary address
|
||||||
if (!buyerSignedPayoutTx.getChangeAmount().equals(new BigInteger("0")) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
if (!buyerSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
||||||
|
|
||||||
// verify sum of outputs = destination amounts + change amount
|
// verify sum of outputs = destination amounts + change amount
|
||||||
if (!buyerSignedPayoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(buyerSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
if (!buyerSignedPayoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(buyerSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
||||||
@ -118,68 +101,15 @@ public class SellerSignAndPublishPayoutTx extends TradeTask {
|
|||||||
// submit fully signed payout tx to the network
|
// submit fully signed payout tx to the network
|
||||||
multisigWallet.submitMultisigTxHex(signedMultisigTxHex);
|
multisigWallet.submitMultisigTxHex(signedMultisigTxHex);
|
||||||
|
|
||||||
// update state
|
// close multisig wallet
|
||||||
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
walletService.closeMultisigWallet(trade.getId());
|
||||||
if (parsedTxSet.getTxs().get(0).getTxSet() != parsedTxSet) System.out.println("LINKS ARE WRONG!!!");
|
|
||||||
trade.setPayoutTx(parsedTxSet.getTxs().get(0));
|
|
||||||
trade.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash());
|
|
||||||
trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
|
|
||||||
complete();
|
|
||||||
|
|
||||||
// checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
|
// update trade state
|
||||||
//
|
parsedTxSet.setMultisigTxHex(signedMultisigTxHex); // TODO (woodser): better place to store this?
|
||||||
// Offer offer = trade.getOffer();
|
trade.setPayoutTx(parsedTxSet.getTxs().get(0));
|
||||||
// TradingPeer tradingPeer = trade.getTradingPeer();
|
trade.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash());
|
||||||
// BtcWalletService walletService = processModel.getBtcWalletService();
|
trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
|
||||||
// String id = processModel.getOffer().getId();
|
complete();
|
||||||
//
|
|
||||||
// final byte[] buyerSignature = tradingPeer.getSignature();
|
|
||||||
//
|
|
||||||
// Coin buyerPayoutAmount = checkNotNull(offer.getBuyerSecurityDeposit()).add(trade.getTradeAmount());
|
|
||||||
// Coin sellerPayoutAmount = offer.getSellerSecurityDeposit();
|
|
||||||
//
|
|
||||||
// final String buyerPayoutAddressString = tradingPeer.getPayoutAddressString();
|
|
||||||
// String sellerPayoutAddressString = walletService.getOrCreateAddressEntry(id,
|
|
||||||
// AddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
|
||||||
//
|
|
||||||
// final byte[] buyerMultiSigPubKey = tradingPeer.getMultiSigPubKey();
|
|
||||||
// byte[] sellerMultiSigPubKey = processModel.getMyMultiSigPubKey();
|
|
||||||
//
|
|
||||||
// Optional<AddressEntry> multiSigAddressEntryOptional = walletService.getAddressEntry(id,
|
|
||||||
// AddressEntry.Context.MULTI_SIG);
|
|
||||||
// if (!multiSigAddressEntryOptional.isPresent() || !Arrays.equals(sellerMultiSigPubKey,
|
|
||||||
// multiSigAddressEntryOptional.get().getPubKey())) {
|
|
||||||
// // In some error edge cases it can be that the address entry is not marked (or was unmarked).
|
|
||||||
// // We do not want to fail in that case and only report a warning.
|
|
||||||
// // One case where that helped to avoid a failed payout attempt was when the taker had a power failure
|
|
||||||
// // at the moment when the offer was taken. This caused first to not see step 1 in the trade process
|
|
||||||
// // (all greyed out) but after the deposit tx was confirmed the trade process was on step 2 and
|
|
||||||
// // everything looked ok. At the payout multiSigAddressEntryOptional was not present and payout
|
|
||||||
// // could not be done. By changing the previous behaviour from fail if multiSigAddressEntryOptional
|
|
||||||
// // is not present to only log a warning the payout worked.
|
|
||||||
// log.warn("sellerMultiSigPubKey from AddressEntry does not match the one from the trade data. " +
|
|
||||||
// "Trade id ={}, multiSigAddressEntryOptional={}", id, multiSigAddressEntryOptional);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(id, sellerMultiSigPubKey);
|
|
||||||
//
|
|
||||||
// Transaction transaction = processModel.getTradeWalletService().sellerSignsAndFinalizesPayoutTx(
|
|
||||||
// checkNotNull(trade.getDepositTx()),
|
|
||||||
// buyerSignature,
|
|
||||||
// buyerPayoutAmount,
|
|
||||||
// sellerPayoutAmount,
|
|
||||||
// buyerPayoutAddressString,
|
|
||||||
// sellerPayoutAddressString,
|
|
||||||
// multiSigKeyPair,
|
|
||||||
// buyerMultiSigPubKey,
|
|
||||||
// sellerMultiSigPubKey
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// trade.setPayoutTx(transaction);
|
|
||||||
//
|
|
||||||
// walletService.swapTradeEntryToAvailableEntry(id, AddressEntry.Context.MULTI_SIG);
|
|
||||||
//
|
|
||||||
// complete();
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
|
@ -41,28 +41,27 @@ public class TakerReservesTradeFunds extends TradeTask {
|
|||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// create transaction to reserve trade
|
// synchronize on wallet to reserve key images
|
||||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
synchronized (model.getXmrWalletService().getWallet()) {
|
||||||
BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
|
|
||||||
BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong());
|
|
||||||
MoneroTxWallet reserveTx = TradeUtils.createReserveTx(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount);
|
|
||||||
|
|
||||||
// freeze trade funds
|
// create transaction to reserve trade
|
||||||
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
|
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
List<String> reserveTxKeyImages = new ArrayList<String>();
|
BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
|
||||||
MoneroWallet wallet = model.getXmrWalletService().getWallet();
|
BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong());
|
||||||
for (MoneroOutput input : reserveTx.getInputs()) {
|
MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount);
|
||||||
reserveTxKeyImages.add(input.getKeyImage().getHex());
|
|
||||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
// collect reserved key images // TODO (woodser): switch to proof of reserve?
|
||||||
|
List<String> reservedKeyImages = new ArrayList<String>();
|
||||||
|
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||||
|
|
||||||
|
// save process state
|
||||||
|
// TODO (woodser): persist
|
||||||
|
processModel.setReserveTx(reserveTx);
|
||||||
|
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
|
||||||
|
trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used?
|
||||||
|
//trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states
|
||||||
|
complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// save process state
|
|
||||||
// TODO (woodser): persist
|
|
||||||
processModel.setReserveTx(reserveTx);
|
|
||||||
processModel.getTaker().setReserveTxKeyImages(reserveTxKeyImages);
|
|
||||||
trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used?
|
|
||||||
//trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states
|
|
||||||
complete();
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
trade.setErrorMessage("An error occurred.\n" +
|
trade.setErrorMessage("An error occurred.\n" +
|
||||||
"Error message:\n"
|
"Error message:\n"
|
||||||
|
@ -42,12 +42,11 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
|
|||||||
try {
|
try {
|
||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// send request to offer signer
|
// send request to arbitrator
|
||||||
sendInitTradeRequest(trade.getOffer().getOfferPayload().getArbitratorSigner(), new SendDirectMessageListener() {
|
sendInitTradeRequest(trade.getOffer().getOfferPayload().getArbitratorSigner(), new SendDirectMessageListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
|
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
|
||||||
complete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// send request to backup arbitrator if signer unavailable
|
// send request to backup arbitrator if signer unavailable
|
||||||
@ -63,7 +62,6 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
|
|||||||
@Override
|
@Override
|
||||||
public void onArrived() {
|
public void onArrived() {
|
||||||
log.info("{} arrived at backup arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
|
log.info("{} arrived at backup arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
|
||||||
complete();
|
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void onFault(String errorMessage) { // TODO (woodser): distinguish nack from offline
|
public void onFault(String errorMessage) { // TODO (woodser): distinguish nack from offline
|
||||||
@ -73,6 +71,7 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
complete(); // TODO (woodser): onArrived() doesn't get called if arbitrator rejects concurrent requests. always complete before onArrived()?
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleErrorMessage(String errorMessage) {
|
public synchronized void handleErrorMessage(String errorMessage) {
|
||||||
// A task runner may call handleErrorMessage(String) more than once.
|
// A task runner may call handleErrorMessage(String) more than once.
|
||||||
// Throw only one exception if that happens, to avoid looping until the
|
// Throw only one exception if that happens, to avoid looping until the
|
||||||
// grpc stream is closed
|
// grpc stream is closed
|
||||||
|
@ -47,7 +47,7 @@ class GrpcExceptionHandler {
|
|||||||
public GrpcExceptionHandler() {
|
public GrpcExceptionHandler() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleException(Logger log,
|
public synchronized void handleException(Logger log,
|
||||||
Throwable t,
|
Throwable t,
|
||||||
StreamObserver<?> responseObserver) {
|
StreamObserver<?> responseObserver) {
|
||||||
// Log the core api error (this is last chance to do that), wrap it in a new
|
// Log the core api error (this is last chance to do that), wrap it in a new
|
||||||
@ -58,7 +58,7 @@ class GrpcExceptionHandler {
|
|||||||
throw grpcStatusRuntimeException;
|
throw grpcStatusRuntimeException;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleExceptionAsWarning(Logger log,
|
public synchronized void handleExceptionAsWarning(Logger log,
|
||||||
String calledMethod,
|
String calledMethod,
|
||||||
Throwable t,
|
Throwable t,
|
||||||
StreamObserver<?> responseObserver) {
|
StreamObserver<?> responseObserver) {
|
||||||
|
@ -8,7 +8,7 @@ import bisq.proto.grpc.NotificationsGrpc.NotificationsImplBase;
|
|||||||
import bisq.proto.grpc.RegisterNotificationListenerRequest;
|
import bisq.proto.grpc.RegisterNotificationListenerRequest;
|
||||||
import bisq.proto.grpc.SendNotificationReply;
|
import bisq.proto.grpc.SendNotificationReply;
|
||||||
import bisq.proto.grpc.SendNotificationRequest;
|
import bisq.proto.grpc.SendNotificationRequest;
|
||||||
|
import io.grpc.Context;
|
||||||
import io.grpc.ServerInterceptor;
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
@ -46,24 +46,30 @@ class GrpcNotificationsService extends NotificationsImplBase {
|
|||||||
@Override
|
@Override
|
||||||
public void registerNotificationListener(RegisterNotificationListenerRequest request,
|
public void registerNotificationListener(RegisterNotificationListenerRequest request,
|
||||||
StreamObserver<NotificationMessage> responseObserver) {
|
StreamObserver<NotificationMessage> responseObserver) {
|
||||||
try {
|
Context ctx = Context.current().fork(); // context is independent for long-lived request
|
||||||
coreApi.addNotificationListener(new GrpcNotificationListener(responseObserver));
|
ctx.run(() -> {
|
||||||
// No onNext / onCompleted, as the response observer should be kept open
|
try {
|
||||||
} catch (Throwable t) {
|
coreApi.addNotificationListener(new GrpcNotificationListener(responseObserver));
|
||||||
exceptionHandler.handleException(log, t, responseObserver);
|
// No onNext / onCompleted, as the response observer should be kept open
|
||||||
}
|
} catch (Throwable t) {
|
||||||
|
exceptionHandler.handleException(log, t, responseObserver);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendNotification(SendNotificationRequest request,
|
public void sendNotification(SendNotificationRequest request,
|
||||||
StreamObserver<SendNotificationReply> responseObserver) {
|
StreamObserver<SendNotificationReply> responseObserver) {
|
||||||
try {
|
Context ctx = Context.current().fork(); // context is independent from notification delivery
|
||||||
coreApi.sendNotification(request.getNotification());
|
ctx.run(() -> {
|
||||||
responseObserver.onNext(SendNotificationReply.newBuilder().build());
|
try {
|
||||||
responseObserver.onCompleted();
|
coreApi.sendNotification(request.getNotification());
|
||||||
} catch (Throwable t) {
|
responseObserver.onNext(SendNotificationReply.newBuilder().build());
|
||||||
exceptionHandler.handleException(log, t, responseObserver);
|
responseObserver.onCompleted();
|
||||||
}
|
} catch (Throwable t) {
|
||||||
|
exceptionHandler.handleException(log, t, responseObserver);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
|
@ -141,6 +141,11 @@ class GrpcOffersService extends OffersImplBase {
|
|||||||
@Override
|
@Override
|
||||||
public void createOffer(CreateOfferRequest req,
|
public void createOffer(CreateOfferRequest req,
|
||||||
StreamObserver<CreateOfferReply> responseObserver) {
|
StreamObserver<CreateOfferReply> responseObserver) {
|
||||||
|
GrpcErrorMessageHandler errorMessageHandler =
|
||||||
|
new GrpcErrorMessageHandler(getCreateOfferMethod().getFullMethodName(),
|
||||||
|
responseObserver,
|
||||||
|
exceptionHandler,
|
||||||
|
log);
|
||||||
try {
|
try {
|
||||||
coreApi.createAnPlaceOffer(
|
coreApi.createAnPlaceOffer(
|
||||||
req.getCurrencyCode(),
|
req.getCurrencyCode(),
|
||||||
@ -162,6 +167,10 @@ class GrpcOffersService extends OffersImplBase {
|
|||||||
.build();
|
.build();
|
||||||
responseObserver.onNext(reply);
|
responseObserver.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
|
},
|
||||||
|
errorMessage -> {
|
||||||
|
if (!errorMessageHandler.isErrorHandled())
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
} catch (Throwable cause) {
|
} catch (Throwable cause) {
|
||||||
exceptionHandler.handleException(log, cause, responseObserver);
|
exceptionHandler.handleException(log, cause, responseObserver);
|
||||||
|
@ -65,7 +65,6 @@ public class GrpcServer {
|
|||||||
GrpcMoneroConnectionsService moneroConnectionsService,
|
GrpcMoneroConnectionsService moneroConnectionsService,
|
||||||
GrpcMoneroNodeService moneroNodeService) {
|
GrpcMoneroNodeService moneroNodeService) {
|
||||||
this.server = ServerBuilder.forPort(config.apiPort)
|
this.server = ServerBuilder.forPort(config.apiPort)
|
||||||
.executor(UserThread.getExecutor())
|
|
||||||
.addService(interceptForward(accountService, accountService.interceptors()))
|
.addService(interceptForward(accountService, accountService.interceptors()))
|
||||||
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
||||||
.addService(interceptForward(disputesService, disputesService.interceptors()))
|
.addService(interceptForward(disputesService, disputesService.interceptors()))
|
||||||
|
@ -19,7 +19,6 @@ package bisq.daemon.grpc;
|
|||||||
|
|
||||||
import bisq.core.api.CoreApi;
|
import bisq.core.api.CoreApi;
|
||||||
import bisq.core.api.model.TradeInfo;
|
import bisq.core.api.model.TradeInfo;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
|
||||||
import bisq.proto.grpc.ConfirmPaymentReceivedReply;
|
import bisq.proto.grpc.ConfirmPaymentReceivedReply;
|
||||||
@ -40,7 +39,6 @@ import bisq.proto.grpc.TakeOfferReply;
|
|||||||
import bisq.proto.grpc.TakeOfferRequest;
|
import bisq.proto.grpc.TakeOfferRequest;
|
||||||
import bisq.proto.grpc.WithdrawFundsReply;
|
import bisq.proto.grpc.WithdrawFundsReply;
|
||||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||||
|
|
||||||
import io.grpc.ServerInterceptor;
|
import io.grpc.ServerInterceptor;
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
|
|
||||||
@ -122,20 +120,24 @@ class GrpcTradesService extends TradesImplBase {
|
|||||||
responseObserver,
|
responseObserver,
|
||||||
exceptionHandler,
|
exceptionHandler,
|
||||||
log);
|
log);
|
||||||
coreApi.takeOffer(req.getOfferId(),
|
try {
|
||||||
req.getPaymentAccountId(),
|
coreApi.takeOffer(req.getOfferId(),
|
||||||
trade -> {
|
req.getPaymentAccountId(),
|
||||||
TradeInfo tradeInfo = toTradeInfo(trade);
|
trade -> {
|
||||||
var reply = TakeOfferReply.newBuilder()
|
TradeInfo tradeInfo = toTradeInfo(trade);
|
||||||
.setTrade(tradeInfo.toProtoMessage())
|
var reply = TakeOfferReply.newBuilder()
|
||||||
.build();
|
.setTrade(tradeInfo.toProtoMessage())
|
||||||
responseObserver.onNext(reply);
|
.build();
|
||||||
responseObserver.onCompleted();
|
responseObserver.onNext(reply);
|
||||||
},
|
responseObserver.onCompleted();
|
||||||
errorMessage -> {
|
},
|
||||||
if (!errorMessageHandler.isErrorHandled())
|
errorMessage -> {
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
if (!errorMessageHandler.isErrorHandled())
|
||||||
});
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
|
});
|
||||||
|
} catch (Throwable cause) {
|
||||||
|
exceptionHandler.handleException(log, cause, responseObserver);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package bisq.daemon.grpc;
|
package bisq.daemon.grpc;
|
||||||
|
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.core.api.CoreApi;
|
import bisq.core.api.CoreApi;
|
||||||
import bisq.core.api.model.AddressBalanceInfo;
|
import bisq.core.api.model.AddressBalanceInfo;
|
||||||
import bisq.core.api.model.TxFeeRateInfo;
|
import bisq.core.api.model.TxFeeRateInfo;
|
||||||
@ -102,16 +103,18 @@ class GrpcWalletsService extends WalletsImplBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void getBalances(GetBalancesRequest req, StreamObserver<GetBalancesReply> responseObserver) {
|
public void getBalances(GetBalancesRequest req, StreamObserver<GetBalancesReply> responseObserver) {
|
||||||
try {
|
UserThread.execute(() -> { // TODO (woodser): Balances.updateBalances() runs on UserThread for JFX components, so call from user thread, else the properties may not be updated. remove JFX properties or push delay into CoreWalletsService.getXmrBalances()?
|
||||||
var balances = coreApi.getBalances(req.getCurrencyCode());
|
try {
|
||||||
var reply = GetBalancesReply.newBuilder()
|
var balances = coreApi.getBalances(req.getCurrencyCode());
|
||||||
.setBalances(balances.toProtoMessage())
|
var reply = GetBalancesReply.newBuilder()
|
||||||
.build();
|
.setBalances(balances.toProtoMessage())
|
||||||
responseObserver.onNext(reply);
|
.build();
|
||||||
responseObserver.onCompleted();
|
responseObserver.onNext(reply);
|
||||||
} catch (Throwable cause) {
|
responseObserver.onCompleted();
|
||||||
exceptionHandler.handleException(log, cause, responseObserver);
|
} catch (Throwable cause) {
|
||||||
}
|
exceptionHandler.handleException(log, cause, responseObserver);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -40,31 +40,37 @@ public class GrpcCallRateMeter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkAndIncrement() {
|
public boolean checkAndIncrement() {
|
||||||
if (getCallsCount() < allowedCallsPerTimeWindow) {
|
synchronized (callTimestamps) {
|
||||||
incrementCallsCount();
|
if (getCallsCount() < allowedCallsPerTimeWindow) {
|
||||||
return true;
|
incrementCallsCount();
|
||||||
} else {
|
return true;
|
||||||
return false;
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getCallsCount() {
|
public int getCallsCount() {
|
||||||
removeStaleCallTimestamps();
|
synchronized (callTimestamps) {
|
||||||
return callTimestamps.size();
|
removeStaleCallTimestamps();
|
||||||
|
return callTimestamps.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCallsCountProgress(String calledMethodName) {
|
public String getCallsCountProgress(String calledMethodName) {
|
||||||
String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase());
|
synchronized (callTimestamps) {
|
||||||
// Just print 'GetVersion has been called N times...',
|
String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase());
|
||||||
// not 'io.bisq.protobuffer.GetVersion/GetVersion has been called N times...'
|
// Just print 'GetVersion has been called N times...',
|
||||||
String loggedMethodName = calledMethodName.split("/")[1];
|
// not 'io.bisq.protobuffer.GetVersion/GetVersion has been called N times...'
|
||||||
return format("%s has been called %d time%s in the last %s, rate limit is %d/%s",
|
String loggedMethodName = calledMethodName.split("/")[1];
|
||||||
loggedMethodName,
|
return format("%s has been called %d time%s in the last %s, rate limit is %d/%s",
|
||||||
callTimestamps.size(),
|
loggedMethodName,
|
||||||
callTimestamps.size() == 1 ? "" : "s",
|
callTimestamps.size(),
|
||||||
shortTimeUnitName,
|
callTimestamps.size() == 1 ? "" : "s",
|
||||||
allowedCallsPerTimeWindow,
|
shortTimeUnitName,
|
||||||
shortTimeUnitName);
|
allowedCallsPerTimeWindow,
|
||||||
|
shortTimeUnitName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void incrementCallsCount() {
|
private void incrementCallsCount() {
|
||||||
@ -85,11 +91,13 @@ public class GrpcCallRateMeter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "GrpcCallRateMeter{" +
|
synchronized (callTimestamps) {
|
||||||
"allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow +
|
return "GrpcCallRateMeter{" +
|
||||||
", timeUnit=" + timeUnit.name() +
|
"allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow +
|
||||||
", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds +
|
", timeUnit=" + timeUnit.name() +
|
||||||
", callsCount=" + callTimestamps.size() +
|
", timeUnitIntervalInMilliseconds=" + timeUnitIntervalInMilliseconds +
|
||||||
'}';
|
", callsCount=" + callTimestamps.size() +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -736,8 +736,10 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> {
|
model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> {
|
||||||
p2PNetworkIcon.setOpacity(1);
|
UserThread.execute(() -> {
|
||||||
p2pNetworkProgressBar.setProgress(0);
|
p2PNetworkIcon.setOpacity(1);
|
||||||
|
p2pNetworkProgressBar.setProgress(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
p2pNetworkProgressBar = new JFXProgressBar(-1);
|
p2pNetworkProgressBar = new JFXProgressBar(-1);
|
||||||
|
@ -240,7 +240,7 @@ public class LockedView extends ActivatableView<VBox, Void> {
|
|||||||
|
|
||||||
private Optional<Tradable> getTradable(LockedListItem item) {
|
private Optional<Tradable> getTradable(LockedListItem item) {
|
||||||
String offerId = item.getAddressEntry().getOfferId();
|
String offerId = item.getAddressEntry().getOfferId();
|
||||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(offerId);
|
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(offerId);
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
return Optional.of(tradeOptional.get());
|
return Optional.of(tradeOptional.get());
|
||||||
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {
|
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {
|
||||||
|
@ -239,7 +239,7 @@ public class ReservedView extends ActivatableView<VBox, Void> {
|
|||||||
|
|
||||||
private Optional<Tradable> getTradable(ReservedListItem item) {
|
private Optional<Tradable> getTradable(ReservedListItem item) {
|
||||||
String offerId = item.getAddressEntry().getOfferId();
|
String offerId = item.getAddressEntry().getOfferId();
|
||||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(offerId);
|
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(offerId);
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
return Optional.of(tradeOptional.get());
|
return Optional.of(tradeOptional.get());
|
||||||
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {
|
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {
|
||||||
|
@ -32,9 +32,7 @@ import javafx.collections.ObservableList;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.daemon.model.MoneroTx;
|
||||||
|
|
||||||
|
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
|
|
||||||
@ -82,14 +80,14 @@ class TransactionAwareTrade implements TransactionAwareTradable {
|
|||||||
|
|
||||||
private boolean isMakerDepositTx(String txId) {
|
private boolean isMakerDepositTx(String txId) {
|
||||||
return Optional.ofNullable(trade.getMakerDepositTx())
|
return Optional.ofNullable(trade.getMakerDepositTx())
|
||||||
.map(MoneroTxWallet::getHash)
|
.map(MoneroTx::getHash)
|
||||||
.map(hash -> hash.equals(txId))
|
.map(hash -> hash.equals(txId))
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTakerDepositTx(String txId) {
|
private boolean isTakerDepositTx(String txId) {
|
||||||
return Optional.ofNullable(trade.getTakerDepositTx())
|
return Optional.ofNullable(trade.getTakerDepositTx())
|
||||||
.map(MoneroTxWallet::getHash)
|
.map(MoneroTx::getHash)
|
||||||
.map(hash -> hash.equals(txId))
|
.map(hash -> hash.equals(txId))
|
||||||
.orElse(false);
|
.orElse(false);
|
||||||
}
|
}
|
||||||
|
@ -382,17 +382,19 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateChartData() {
|
private void updateChartData() {
|
||||||
seriesBuy.getData().clear();
|
UserThread.execute(() -> {
|
||||||
seriesSell.getData().clear();
|
seriesBuy.getData().clear();
|
||||||
areaChart.getData().clear();
|
seriesSell.getData().clear();
|
||||||
|
areaChart.getData().clear();
|
||||||
|
|
||||||
boolean isCrypto = CurrencyUtil.isCryptoCurrency(model.getCurrencyCode());
|
boolean isCrypto = CurrencyUtil.isCryptoCurrency(model.getCurrencyCode());
|
||||||
|
|
||||||
// crypto: left-sell, right-buy. fiat: left-buy, right-sell
|
// crypto: left-sell, right-buy. fiat: left-buy, right-sell
|
||||||
seriesBuy.getData().addAll(filterOutliersBuy(model.getBuyData(), isCrypto));
|
seriesBuy.getData().addAll(filterOutliersBuy(model.getBuyData(), isCrypto));
|
||||||
seriesSell.getData().addAll(filterOutliersSell(model.getSellData(), isCrypto));
|
seriesSell.getData().addAll(filterOutliersSell(model.getSellData(), isCrypto));
|
||||||
|
|
||||||
areaChart.getData().addAll(List.of(seriesBuy, seriesSell));
|
areaChart.getData().addAll(List.of(seriesBuy, seriesSell));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<XYChart.Data<Number, Number>> filterOutliersBuy(List<XYChart.Data<Number, Number>> buy, boolean isCrypto) {
|
List<XYChart.Data<Number, Number>> filterOutliersBuy(List<XYChart.Data<Number, Number>> buy, boolean isCrypto) {
|
||||||
|
@ -62,7 +62,7 @@ import bisq.core.util.FormattingUtils;
|
|||||||
import bisq.core.util.coin.CoinFormatter;
|
import bisq.core.util.coin.CoinFormatter;
|
||||||
|
|
||||||
import bisq.network.p2p.NodeAddress;
|
import bisq.network.p2p.NodeAddress;
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.common.app.DevEnv;
|
import bisq.common.app.DevEnv;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.util.Tuple3;
|
import bisq.common.util.Tuple3;
|
||||||
@ -305,7 +305,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
|
|||||||
GridPane.setMargin(nrOfOffersLabel, new Insets(10, 0, 0, 0));
|
GridPane.setMargin(nrOfOffersLabel, new Insets(10, 0, 0, 0));
|
||||||
root.getChildren().add(nrOfOffersLabel);
|
root.getChildren().add(nrOfOffersLabel);
|
||||||
|
|
||||||
offerListListener = c -> nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size()));
|
offerListListener = c -> UserThread.execute(() -> nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size())));
|
||||||
|
|
||||||
// Fixes incorrect ordering of Available offers:
|
// Fixes incorrect ordering of Available offers:
|
||||||
// https://github.com/bisq-network/bisq-desktop/issues/588
|
// https://github.com/bisq-network/bisq-desktop/issues/588
|
||||||
|
@ -51,7 +51,7 @@ import bisq.network.p2p.P2PService;
|
|||||||
import bisq.network.p2p.network.CloseConnectionReason;
|
import bisq.network.p2p.network.CloseConnectionReason;
|
||||||
import bisq.network.p2p.network.Connection;
|
import bisq.network.p2p.network.Connection;
|
||||||
import bisq.network.p2p.network.ConnectionListener;
|
import bisq.network.p2p.network.ConnectionListener;
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.common.app.DevEnv;
|
import bisq.common.app.DevEnv;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
@ -524,23 +524,25 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateSpinnerInfo() {
|
private void updateSpinnerInfo() {
|
||||||
if (!showPayFundsScreenDisplayed.get() ||
|
UserThread.execute(() -> {
|
||||||
offerWarning.get() != null ||
|
if (!showPayFundsScreenDisplayed.get() ||
|
||||||
errorMessage.get() != null ||
|
offerWarning.get() != null ||
|
||||||
showTransactionPublishedScreen.get()) {
|
errorMessage.get() != null ||
|
||||||
spinnerInfoText.set("");
|
showTransactionPublishedScreen.get()) {
|
||||||
} else if (dataModel.getIsBtcWalletFunded().get()) {
|
|
||||||
spinnerInfoText.set("");
|
|
||||||
/* if (dataModel.isFeeFromFundingTxSufficient.get()) {
|
|
||||||
spinnerInfoText.set("");
|
spinnerInfoText.set("");
|
||||||
|
} else if (dataModel.getIsBtcWalletFunded().get()) {
|
||||||
|
spinnerInfoText.set("");
|
||||||
|
/* if (dataModel.isFeeFromFundingTxSufficient.get()) {
|
||||||
|
spinnerInfoText.set("");
|
||||||
|
} else {
|
||||||
|
spinnerInfoText.set("Check if funding tx miner fee is sufficient...");
|
||||||
|
}*/
|
||||||
} else {
|
} else {
|
||||||
spinnerInfoText.set("Check if funding tx miner fee is sufficient...");
|
spinnerInfoText.set(Res.get("shared.waitingForFunds"));
|
||||||
}*/
|
}
|
||||||
} else {
|
|
||||||
spinnerInfoText.set(Res.get("shared.waitingForFunds"));
|
|
||||||
}
|
|
||||||
|
|
||||||
isWaitingForFunds.set(!spinnerInfoText.get().isEmpty());
|
isWaitingForFunds.set(!spinnerInfoText.get().isEmpty());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addListeners() {
|
private void addListeners() {
|
||||||
|
@ -515,47 +515,49 @@ public abstract class Overlay<T extends Overlay<T>> {
|
|||||||
if (owner != null) {
|
if (owner != null) {
|
||||||
Scene rootScene = owner.getScene();
|
Scene rootScene = owner.getScene();
|
||||||
if (rootScene != null) {
|
if (rootScene != null) {
|
||||||
Scene scene = new Scene(getRootContainer());
|
UserThread.execute(() -> {
|
||||||
scene.getStylesheets().setAll(rootScene.getStylesheets());
|
Scene scene = new Scene(getRootContainer());
|
||||||
scene.setFill(Color.TRANSPARENT);
|
scene.getStylesheets().setAll(rootScene.getStylesheets());
|
||||||
|
scene.setFill(Color.TRANSPARENT);
|
||||||
|
|
||||||
setupKeyHandler(scene);
|
setupKeyHandler(scene);
|
||||||
|
|
||||||
stage = new Stage();
|
stage = new Stage();
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
Window window = rootScene.getWindow();
|
Window window = rootScene.getWindow();
|
||||||
setModality();
|
setModality();
|
||||||
stage.initStyle(StageStyle.TRANSPARENT);
|
stage.initStyle(StageStyle.TRANSPARENT);
|
||||||
stage.setOnCloseRequest(event -> {
|
stage.setOnCloseRequest(event -> {
|
||||||
event.consume();
|
event.consume();
|
||||||
doClose();
|
doClose();
|
||||||
|
});
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.show();
|
||||||
|
|
||||||
|
layout();
|
||||||
|
|
||||||
|
addEffectToBackground();
|
||||||
|
|
||||||
|
// On Linux the owner stage does not move the child stage as it does on Mac
|
||||||
|
// So we need to apply centerPopup. Further with fast movements the handler loses
|
||||||
|
// the latest position, with a delay it fixes that.
|
||||||
|
// Also on Mac sometimes the popups are positioned outside of the main app, so keep it for all OS
|
||||||
|
positionListener = (observable, oldValue, newValue) -> {
|
||||||
|
if (stage != null) {
|
||||||
|
layout();
|
||||||
|
if (centerTime != null)
|
||||||
|
centerTime.stop();
|
||||||
|
|
||||||
|
centerTime = UserThread.runAfter(this::layout, 3);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.xProperty().addListener(positionListener);
|
||||||
|
window.yProperty().addListener(positionListener);
|
||||||
|
window.widthProperty().addListener(positionListener);
|
||||||
|
|
||||||
|
animateDisplay();
|
||||||
|
isDisplayed = true;
|
||||||
});
|
});
|
||||||
stage.sizeToScene();
|
|
||||||
stage.show();
|
|
||||||
|
|
||||||
layout();
|
|
||||||
|
|
||||||
addEffectToBackground();
|
|
||||||
|
|
||||||
// On Linux the owner stage does not move the child stage as it does on Mac
|
|
||||||
// So we need to apply centerPopup. Further with fast movements the handler loses
|
|
||||||
// the latest position, with a delay it fixes that.
|
|
||||||
// Also on Mac sometimes the popups are positioned outside of the main app, so keep it for all OS
|
|
||||||
positionListener = (observable, oldValue, newValue) -> {
|
|
||||||
if (stage != null) {
|
|
||||||
layout();
|
|
||||||
if (centerTime != null)
|
|
||||||
centerTime.stop();
|
|
||||||
|
|
||||||
centerTime = UserThread.runAfter(this::layout, 3);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.xProperty().addListener(positionListener);
|
|
||||||
window.yProperty().addListener(positionListener);
|
|
||||||
window.widthProperty().addListener(positionListener);
|
|
||||||
|
|
||||||
animateDisplay();
|
|
||||||
isDisplayed = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,9 @@ public class Notification extends Overlay<Notification> {
|
|||||||
if (autoClose && autoCloseTimer == null)
|
if (autoClose && autoCloseTimer == null)
|
||||||
autoCloseTimer = UserThread.runAfter(this::doClose, 6);
|
autoCloseTimer = UserThread.runAfter(this::doClose, 6);
|
||||||
|
|
||||||
stage.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> doClose());
|
UserThread.execute(() -> {
|
||||||
|
stage.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> doClose());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -54,7 +54,7 @@ import bisq.core.trade.protocol.SellerProtocol;
|
|||||||
import bisq.core.user.Preferences;
|
import bisq.core.user.Preferences;
|
||||||
|
|
||||||
import bisq.network.p2p.P2PService;
|
import bisq.network.p2p.P2PService;
|
||||||
|
import bisq.common.UserThread;
|
||||||
import bisq.common.crypto.PubKeyRing;
|
import bisq.common.crypto.PubKeyRing;
|
||||||
import bisq.common.crypto.PubKeyRingProvider;
|
import bisq.common.crypto.PubKeyRingProvider;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
@ -87,8 +87,7 @@ import javax.annotation.Nullable;
|
|||||||
import static com.google.common.base.Preconditions.checkArgument;
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import monero.daemon.model.MoneroTx;
|
||||||
|
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
@ -360,54 +359,56 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doSelectItem(@Nullable PendingTradesListItem item) {
|
private void doSelectItem(@Nullable PendingTradesListItem item) {
|
||||||
if (selectedTrade != null)
|
UserThread.execute(() -> {
|
||||||
selectedTrade.stateProperty().removeListener(tradeStateChangeListener);
|
if (selectedTrade != null)
|
||||||
|
selectedTrade.stateProperty().removeListener(tradeStateChangeListener);
|
||||||
|
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
selectedTrade = item.getTrade();
|
selectedTrade = item.getTrade();
|
||||||
if (selectedTrade == null) {
|
if (selectedTrade == null) {
|
||||||
log.error("selectedTrade is null");
|
log.error("selectedTrade is null");
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
MoneroTxWallet makerDepositTx = selectedTrade.getMakerDepositTx();
|
|
||||||
MoneroTxWallet takerDepositTx = selectedTrade.getTakerDepositTx();
|
|
||||||
String tradeId = selectedTrade.getId();
|
|
||||||
tradeStateChangeListener = (observable, oldValue, newValue) -> {
|
|
||||||
if (makerDepositTx != null && takerDepositTx != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable
|
|
||||||
makerTxId.set(makerDepositTx.getHash());
|
|
||||||
takerTxId.set(takerDepositTx.getHash());
|
|
||||||
notificationCenter.setSelectedTradeId(tradeId);
|
|
||||||
selectedTrade.stateProperty().removeListener(tradeStateChangeListener);
|
|
||||||
} else {
|
|
||||||
makerTxId.set("");
|
|
||||||
takerTxId.set("");
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
selectedTrade.stateProperty().addListener(tradeStateChangeListener);
|
|
||||||
|
|
||||||
Offer offer = selectedTrade.getOffer();
|
MoneroTx makerDepositTx = selectedTrade.getMakerDepositTx();
|
||||||
if (offer == null) {
|
MoneroTx takerDepositTx = selectedTrade.getTakerDepositTx();
|
||||||
log.error("offer is null");
|
String tradeId = selectedTrade.getId();
|
||||||
return;
|
tradeStateChangeListener = (observable, oldValue, newValue) -> {
|
||||||
}
|
if (makerDepositTx != null && takerDepositTx != null) { // TODO (woodser): this treats separate deposit ids as one unit, being both available or unavailable
|
||||||
|
makerTxId.set(makerDepositTx.getHash());
|
||||||
|
takerTxId.set(takerDepositTx.getHash());
|
||||||
|
notificationCenter.setSelectedTradeId(tradeId);
|
||||||
|
selectedTrade.stateProperty().removeListener(tradeStateChangeListener);
|
||||||
|
} else {
|
||||||
|
makerTxId.set("");
|
||||||
|
takerTxId.set("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
selectedTrade.stateProperty().addListener(tradeStateChangeListener);
|
||||||
|
|
||||||
isMaker = tradeManager.isMyOffer(offer);
|
Offer offer = selectedTrade.getOffer();
|
||||||
if (makerDepositTx != null && takerDepositTx != null) {
|
if (offer == null) {
|
||||||
makerTxId.set(makerDepositTx.getHash());
|
log.error("offer is null");
|
||||||
takerTxId.set(takerDepositTx.getHash());
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMaker = tradeManager.isMyOffer(offer);
|
||||||
|
if (makerDepositTx != null && takerDepositTx != null) {
|
||||||
|
makerTxId.set(makerDepositTx.getHash());
|
||||||
|
takerTxId.set(takerDepositTx.getHash());
|
||||||
|
} else {
|
||||||
|
makerTxId.set("");
|
||||||
|
takerTxId.set("");
|
||||||
|
}
|
||||||
|
notificationCenter.setSelectedTradeId(tradeId);
|
||||||
} else {
|
} else {
|
||||||
|
selectedTrade = null;
|
||||||
makerTxId.set("");
|
makerTxId.set("");
|
||||||
takerTxId.set("");
|
takerTxId.set("");
|
||||||
|
notificationCenter.setSelectedTradeId(null);
|
||||||
}
|
}
|
||||||
notificationCenter.setSelectedTradeId(tradeId);
|
selectedItemProperty.set(item);
|
||||||
} else {
|
});
|
||||||
selectedTrade = null;
|
|
||||||
makerTxId.set("");
|
|
||||||
takerTxId.set("");
|
|
||||||
notificationCenter.setSelectedTradeId(null);
|
|
||||||
}
|
|
||||||
selectedItemProperty.set(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tryOpenDispute(boolean isSupportTicket) {
|
private void tryOpenDispute(boolean isSupportTicket) {
|
||||||
@ -458,8 +459,9 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||||||
byte[] payoutTxSerialized = null;
|
byte[] payoutTxSerialized = null;
|
||||||
String payoutTxHashAsString = null;
|
String payoutTxHashAsString = null;
|
||||||
MoneroTxWallet payoutTx = trade.getPayoutTx();
|
MoneroTxWallet payoutTx = trade.getPayoutTx();
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
||||||
String updatedMultisigHex = multisigWallet.getMultisigHex();
|
String updatedMultisigHex = multisigWallet.getMultisigHex();
|
||||||
|
xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet
|
||||||
if (payoutTx != null) {
|
if (payoutTx != null) {
|
||||||
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
|
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
|
||||||
// payoutTxHashAsString = payoutTx.getHashAsString();
|
// payoutTxHashAsString = payoutTx.getHashAsString();
|
||||||
|
@ -363,7 +363,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateMoveTradeToFailedColumnState() {
|
private void updateMoveTradeToFailedColumnState() {
|
||||||
moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade())));
|
UserThread.execute(() -> moveTradeToFailedColumn.setVisible(model.dataModel.list.stream().anyMatch(item -> isMaybeInvalidTrade(item.getTrade()))));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isMaybeInvalidTrade(Trade trade) {
|
private boolean isMaybeInvalidTrade(Trade trade) {
|
||||||
|
@ -137,7 +137,8 @@ public class BuyerStep2View extends TradeStepView {
|
|||||||
showPopup();
|
showPopup();
|
||||||
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG.ordinal()) {
|
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG.ordinal()) {
|
||||||
if (!trade.hasFailed()) {
|
if (!trade.hasFailed()) {
|
||||||
switch (state) {
|
UserThread.execute(() -> {
|
||||||
|
switch (state) {
|
||||||
case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED:
|
case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED:
|
||||||
case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG:
|
case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG:
|
||||||
busyAnimation.play();
|
busyAnimation.play();
|
||||||
@ -169,7 +170,8 @@ public class BuyerStep2View extends TradeStepView {
|
|||||||
busyAnimation.stop();
|
busyAnimation.stop();
|
||||||
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
|
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
log.warn("Trade contains error message {}", trade.getErrorMessage());
|
log.warn("Trade contains error message {}", trade.getErrorMessage());
|
||||||
statusLabel.setText("");
|
statusLabel.setText("");
|
||||||
|
@ -471,43 +471,45 @@ public class ChatView extends AnchorPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateMsgState(ChatMessage message) {
|
private void updateMsgState(ChatMessage message) {
|
||||||
boolean visible;
|
UserThread.execute(() -> {
|
||||||
AwesomeIcon icon = null;
|
boolean visible;
|
||||||
String text = null;
|
AwesomeIcon icon = null;
|
||||||
statusIcon.getStyleClass().add("status-icon");
|
String text = null;
|
||||||
statusInfoLabel.getStyleClass().add("status-icon");
|
statusIcon.getStyleClass().add("status-icon");
|
||||||
statusHBox.setOpacity(1);
|
statusInfoLabel.getStyleClass().add("status-icon");
|
||||||
log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(),
|
statusHBox.setOpacity(1);
|
||||||
message.acknowledgedProperty().get(), message.arrivedProperty().get());
|
log.debug("updateMsgState msg-{}, ack={}, arrived={}", message.getMessage(),
|
||||||
if (message.acknowledgedProperty().get()) {
|
message.acknowledgedProperty().get(), message.arrivedProperty().get());
|
||||||
visible = true;
|
if (message.acknowledgedProperty().get()) {
|
||||||
icon = AwesomeIcon.OK_SIGN;
|
visible = true;
|
||||||
text = Res.get("support.acknowledged");
|
icon = AwesomeIcon.OK_SIGN;
|
||||||
} else if (message.ackErrorProperty().get() != null) {
|
text = Res.get("support.acknowledged");
|
||||||
visible = true;
|
} else if (message.ackErrorProperty().get() != null) {
|
||||||
icon = AwesomeIcon.EXCLAMATION_SIGN;
|
visible = true;
|
||||||
text = Res.get("support.error", message.ackErrorProperty().get());
|
icon = AwesomeIcon.EXCLAMATION_SIGN;
|
||||||
statusIcon.getStyleClass().add("error-text");
|
text = Res.get("support.error", message.ackErrorProperty().get());
|
||||||
statusInfoLabel.getStyleClass().add("error-text");
|
statusIcon.getStyleClass().add("error-text");
|
||||||
} else if (message.arrivedProperty().get()) {
|
statusInfoLabel.getStyleClass().add("error-text");
|
||||||
visible = true;
|
} else if (message.arrivedProperty().get()) {
|
||||||
icon = AwesomeIcon.OK;
|
visible = true;
|
||||||
text = Res.get("support.arrived");
|
icon = AwesomeIcon.OK;
|
||||||
} else if (message.storedInMailboxProperty().get()) {
|
text = Res.get("support.arrived");
|
||||||
visible = true;
|
} else if (message.storedInMailboxProperty().get()) {
|
||||||
icon = AwesomeIcon.ENVELOPE;
|
visible = true;
|
||||||
text = Res.get("support.savedInMailbox");
|
icon = AwesomeIcon.ENVELOPE;
|
||||||
} else {
|
text = Res.get("support.savedInMailbox");
|
||||||
visible = false;
|
} else {
|
||||||
log.debug("updateMsgState called but no msg state available. message={}", message);
|
visible = false;
|
||||||
}
|
log.debug("updateMsgState called but no msg state available. message={}", message);
|
||||||
|
}
|
||||||
|
|
||||||
statusHBox.setVisible(visible);
|
statusHBox.setVisible(visible);
|
||||||
if (visible) {
|
if (visible) {
|
||||||
AwesomeDude.setIcon(statusIcon, icon, "14");
|
AwesomeDude.setIcon(statusIcon, icon, "14");
|
||||||
statusIcon.setTooltip(new Tooltip(text));
|
statusIcon.setTooltip(new Tooltip(text));
|
||||||
statusInfoLabel.setText(text);
|
statusInfoLabel.setText(text);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -85,76 +85,78 @@ public class DisputeChatPopup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSession, String counterpartyName) {
|
public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSession, String counterpartyName) {
|
||||||
closeChat();
|
UserThread.execute(() -> {
|
||||||
this.selectedDispute = selectedDispute;
|
closeChat();
|
||||||
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
|
this.selectedDispute = selectedDispute;
|
||||||
disputeManager.requestPersistence();
|
|
||||||
|
|
||||||
ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName);
|
|
||||||
chatView.setAllowAttachments(true);
|
|
||||||
chatView.setDisplayHeader(false);
|
|
||||||
chatView.initialize();
|
|
||||||
|
|
||||||
AnchorPane pane = new AnchorPane(chatView);
|
|
||||||
pane.setPrefSize(760, 500);
|
|
||||||
AnchorPane.setLeftAnchor(chatView, 10d);
|
|
||||||
AnchorPane.setRightAnchor(chatView, 10d);
|
|
||||||
AnchorPane.setTopAnchor(chatView, -20d);
|
|
||||||
AnchorPane.setBottomAnchor(chatView, 10d);
|
|
||||||
pane.getStyleClass().add("dispute-chat-border");
|
|
||||||
Button closeDisputeButton = null;
|
|
||||||
if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) {
|
|
||||||
closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
|
|
||||||
closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute));
|
|
||||||
}
|
|
||||||
chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty());
|
|
||||||
chatView.activate();
|
|
||||||
chatView.scrollToBottom();
|
|
||||||
chatPopupStage = new Stage();
|
|
||||||
chatPopupStage.setTitle(Res.get("disputeChat.chatWindowTitle", selectedDispute.getShortTradeId())
|
|
||||||
+ " " + selectedDispute.getRoleString());
|
|
||||||
StackPane owner = MainView.getRootContainer();
|
|
||||||
Scene rootScene = owner.getScene();
|
|
||||||
chatPopupStage.initOwner(rootScene.getWindow());
|
|
||||||
chatPopupStage.initModality(Modality.NONE);
|
|
||||||
chatPopupStage.initStyle(StageStyle.DECORATED);
|
|
||||||
chatPopupStage.setOnHiding(event -> {
|
|
||||||
chatView.deactivate();
|
|
||||||
// at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon.
|
|
||||||
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
|
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
|
||||||
disputeManager.requestPersistence();
|
disputeManager.requestPersistence();
|
||||||
chatPopupStage = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
Scene scene = new Scene(pane);
|
ChatView chatView = new ChatView(disputeManager, formatter, counterpartyName);
|
||||||
CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), false);
|
chatView.setAllowAttachments(true);
|
||||||
scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> {
|
chatView.setDisplayHeader(false);
|
||||||
if (ev.getCode() == KeyCode.ESCAPE) {
|
chatView.initialize();
|
||||||
ev.consume();
|
|
||||||
chatPopupStage.hide();
|
AnchorPane pane = new AnchorPane(chatView);
|
||||||
|
pane.setPrefSize(760, 500);
|
||||||
|
AnchorPane.setLeftAnchor(chatView, 10d);
|
||||||
|
AnchorPane.setRightAnchor(chatView, 10d);
|
||||||
|
AnchorPane.setTopAnchor(chatView, -20d);
|
||||||
|
AnchorPane.setBottomAnchor(chatView, 10d);
|
||||||
|
pane.getStyleClass().add("dispute-chat-border");
|
||||||
|
Button closeDisputeButton = null;
|
||||||
|
if (!selectedDispute.isClosed() && !disputeManager.isTrader(selectedDispute)) {
|
||||||
|
closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket"));
|
||||||
|
closeDisputeButton.setOnAction(e -> chatCallback.onCloseDisputeFromChatWindow(selectedDispute));
|
||||||
}
|
}
|
||||||
|
chatView.display(concreteDisputeSession, closeDisputeButton, pane.widthProperty());
|
||||||
|
chatView.activate();
|
||||||
|
chatView.scrollToBottom();
|
||||||
|
chatPopupStage = new Stage();
|
||||||
|
chatPopupStage.setTitle(Res.get("disputeChat.chatWindowTitle", selectedDispute.getShortTradeId())
|
||||||
|
+ " " + selectedDispute.getRoleString());
|
||||||
|
StackPane owner = MainView.getRootContainer();
|
||||||
|
Scene rootScene = owner.getScene();
|
||||||
|
chatPopupStage.initOwner(rootScene.getWindow());
|
||||||
|
chatPopupStage.initModality(Modality.NONE);
|
||||||
|
chatPopupStage.initStyle(StageStyle.DECORATED);
|
||||||
|
chatPopupStage.setOnHiding(event -> {
|
||||||
|
chatView.deactivate();
|
||||||
|
// at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon.
|
||||||
|
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
|
||||||
|
disputeManager.requestPersistence();
|
||||||
|
chatPopupStage = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
Scene scene = new Scene(pane);
|
||||||
|
CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), false);
|
||||||
|
scene.addEventHandler(KeyEvent.KEY_RELEASED, ev -> {
|
||||||
|
if (ev.getCode() == KeyCode.ESCAPE) {
|
||||||
|
ev.consume();
|
||||||
|
chatPopupStage.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
chatPopupStage.setScene(scene);
|
||||||
|
chatPopupStage.setOpacity(0);
|
||||||
|
chatPopupStage.show();
|
||||||
|
|
||||||
|
xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue;
|
||||||
|
chatPopupStage.xProperty().addListener(xPositionListener);
|
||||||
|
yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue;
|
||||||
|
chatPopupStage.yProperty().addListener(yPositionListener);
|
||||||
|
|
||||||
|
if (chatPopupStageXPosition == -1) {
|
||||||
|
Window rootSceneWindow = rootScene.getWindow();
|
||||||
|
double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight();
|
||||||
|
chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3)));
|
||||||
|
chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3)));
|
||||||
|
} else {
|
||||||
|
chatPopupStage.setX(chatPopupStageXPosition);
|
||||||
|
chatPopupStage.setY(chatPopupStageYPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay display to next render frame to avoid that the popup is first quickly displayed in default position
|
||||||
|
// and after a short moment in the correct position
|
||||||
|
UserThread.execute(() -> chatPopupStage.setOpacity(1));
|
||||||
});
|
});
|
||||||
chatPopupStage.setScene(scene);
|
|
||||||
chatPopupStage.setOpacity(0);
|
|
||||||
chatPopupStage.show();
|
|
||||||
|
|
||||||
xPositionListener = (observable, oldValue, newValue) -> chatPopupStageXPosition = (double) newValue;
|
|
||||||
chatPopupStage.xProperty().addListener(xPositionListener);
|
|
||||||
yPositionListener = (observable, oldValue, newValue) -> chatPopupStageYPosition = (double) newValue;
|
|
||||||
chatPopupStage.yProperty().addListener(yPositionListener);
|
|
||||||
|
|
||||||
if (chatPopupStageXPosition == -1) {
|
|
||||||
Window rootSceneWindow = rootScene.getWindow();
|
|
||||||
double titleBarHeight = rootSceneWindow.getHeight() - rootScene.getHeight();
|
|
||||||
chatPopupStage.setX(Math.round(rootSceneWindow.getX() + (owner.getWidth() - chatPopupStage.getWidth() / 4 * 3)));
|
|
||||||
chatPopupStage.setY(Math.round(rootSceneWindow.getY() + titleBarHeight + (owner.getHeight() - chatPopupStage.getHeight() / 4 * 3)));
|
|
||||||
} else {
|
|
||||||
chatPopupStage.setX(chatPopupStageXPosition);
|
|
||||||
chatPopupStage.setY(chatPopupStageYPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delay display to next render frame to avoid that the popup is first quickly displayed in default position
|
|
||||||
// and after a short moment in the correct position
|
|
||||||
UserThread.execute(() -> chatPopupStage.setOpacity(1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1131,7 +1131,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
if (item != null && !empty) {
|
if (item != null && !empty) {
|
||||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(item.getTradeId());
|
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(item.getTradeId());
|
||||||
if (tradeOptional.isPresent()) {
|
if (tradeOptional.isPresent()) {
|
||||||
field = new HyperlinkWithIcon(item.getShortTradeId());
|
field = new HyperlinkWithIcon(item.getShortTradeId());
|
||||||
field.setMouseTransparent(false);
|
field.setMouseTransparent(false);
|
||||||
@ -1349,31 +1349,33 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||||||
@Override
|
@Override
|
||||||
public void updateItem(final Dispute item, boolean empty) {
|
public void updateItem(final Dispute item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
if (item != null && !empty) {
|
UserThread.execute(() -> {
|
||||||
if (closedProperty != null) {
|
if (item != null && !empty) {
|
||||||
closedProperty.removeListener(listener);
|
if (closedProperty != null) {
|
||||||
}
|
closedProperty.removeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
listener = (observable, oldValue, newValue) -> {
|
listener = (observable, oldValue, newValue) -> {
|
||||||
setText(newValue ? Res.get("support.closed") : Res.get("support.open"));
|
setText(newValue ? Res.get("support.closed") : Res.get("support.open"));
|
||||||
|
if (getTableRow() != null)
|
||||||
|
getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
||||||
|
if (item.isClosed() && item == chatPopup.getSelectedDispute())
|
||||||
|
chatPopup.closeChat(); // close the chat popup when the associated ticket is closed
|
||||||
|
};
|
||||||
|
closedProperty = item.isClosedProperty();
|
||||||
|
closedProperty.addListener(listener);
|
||||||
|
boolean isClosed = item.isClosed();
|
||||||
|
setText(isClosed ? Res.get("support.closed") : Res.get("support.open"));
|
||||||
if (getTableRow() != null)
|
if (getTableRow() != null)
|
||||||
getTableRow().setOpacity(newValue && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
||||||
if (item.isClosed() && item == chatPopup.getSelectedDispute())
|
} else {
|
||||||
chatPopup.closeChat(); // close the chat popup when the associated ticket is closed
|
if (closedProperty != null) {
|
||||||
};
|
closedProperty.removeListener(listener);
|
||||||
closedProperty = item.isClosedProperty();
|
closedProperty = null;
|
||||||
closedProperty.addListener(listener);
|
}
|
||||||
boolean isClosed = item.isClosed();
|
setText("");
|
||||||
setText(isClosed ? Res.get("support.closed") : Res.get("support.open"));
|
|
||||||
if (getTableRow() != null)
|
|
||||||
getTableRow().setOpacity(isClosed && item.getBadgeCountProperty().get() == 0 ? 0.4 : 1);
|
|
||||||
} else {
|
|
||||||
if (closedProperty != null) {
|
|
||||||
closedProperty.removeListener(listener);
|
|
||||||
closedProperty = null;
|
|
||||||
}
|
}
|
||||||
setText("");
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1389,27 +1391,29 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateChatMessageCount(Dispute dispute, JFXBadge chatBadge) {
|
private void updateChatMessageCount(Dispute dispute, JFXBadge chatBadge) {
|
||||||
if (chatBadge == null)
|
UserThread.execute(() -> {
|
||||||
return;
|
if (chatBadge == null)
|
||||||
// when the chat popup is active, we do not display new message count indicator for that item
|
return;
|
||||||
if (chatPopup.isChatShown() && selectedDispute != null && dispute.getId().equals(selectedDispute.getId())) {
|
// when the chat popup is active, we do not display new message count indicator for that item
|
||||||
chatBadge.setText("");
|
if (chatPopup.isChatShown() && selectedDispute != null && dispute.getId().equals(selectedDispute.getId())) {
|
||||||
chatBadge.setEnabled(false);
|
chatBadge.setText("");
|
||||||
chatBadge.refreshBadge();
|
chatBadge.setEnabled(false);
|
||||||
// have to UserThread.execute or the new message will be sent to peer as "read"
|
chatBadge.refreshBadge();
|
||||||
UserThread.execute(() -> dispute.setChatMessagesSeen(senderFlag()));
|
// have to UserThread.execute or the new message will be sent to peer as "read"
|
||||||
return;
|
UserThread.execute(() -> dispute.setChatMessagesSeen(senderFlag()));
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (dispute.unreadMessageCount(senderFlag()) > 0) {
|
if (dispute.unreadMessageCount(senderFlag()) > 0) {
|
||||||
chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag())));
|
chatBadge.setText(String.valueOf(dispute.unreadMessageCount(senderFlag())));
|
||||||
chatBadge.setEnabled(true);
|
chatBadge.setEnabled(true);
|
||||||
} else {
|
} else {
|
||||||
chatBadge.setText("");
|
chatBadge.setText("");
|
||||||
chatBadge.setEnabled(false);
|
chatBadge.setEnabled(false);
|
||||||
}
|
}
|
||||||
chatBadge.refreshBadge();
|
chatBadge.refreshBadge();
|
||||||
dispute.refreshAlertLevel(senderFlag());
|
dispute.refreshAlertLevel(senderFlag());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getCounterpartyName() {
|
private String getCounterpartyName() {
|
||||||
|
@ -92,12 +92,12 @@ public class Transitions {
|
|||||||
public void fadeOutAndRemove(Node node, int duration, EventHandler<ActionEvent> handler) {
|
public void fadeOutAndRemove(Node node, int duration, EventHandler<ActionEvent> handler) {
|
||||||
FadeTransition fade = fadeOut(node, getDuration(duration));
|
FadeTransition fade = fadeOut(node, getDuration(duration));
|
||||||
fade.setInterpolator(Interpolator.EASE_IN);
|
fade.setInterpolator(Interpolator.EASE_IN);
|
||||||
fade.setOnFinished(actionEvent -> {
|
fade.setOnFinished(actionEvent -> UserThread.execute(() -> {
|
||||||
((Pane) (node.getParent())).getChildren().remove(node);
|
((Pane) (node.getParent())).getChildren().remove(node);
|
||||||
//Profiler.printMsgWithTime("fadeOutAndRemove");
|
//Profiler.printMsgWithTime("fadeOutAndRemove");
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
handler.handle(actionEvent);
|
handler.handle(actionEvent);
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blur
|
// Blur
|
||||||
|
@ -112,6 +112,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
private static final int MAX_PERMITTED_MESSAGE_SIZE = 10 * 1024 * 1024; // 10 MB (425 offers resulted in about 660 kb, mailbox msg will add more to it) offer has usually 2 kb, mailbox 3kb.
|
private static final int MAX_PERMITTED_MESSAGE_SIZE = 10 * 1024 * 1024; // 10 MB (425 offers resulted in about 660 kb, mailbox msg will add more to it) offer has usually 2 kb, mailbox 3kb.
|
||||||
//TODO decrease limits again after testing
|
//TODO decrease limits again after testing
|
||||||
private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(180);
|
private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(180);
|
||||||
|
private static final int MAX_CONNECTION_THREADS = 10;
|
||||||
|
|
||||||
public static int getPermittedMessageSize() {
|
public static int getPermittedMessageSize() {
|
||||||
return PERMITTED_MESSAGE_SIZE;
|
return PERMITTED_MESSAGE_SIZE;
|
||||||
@ -130,6 +131,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
@Getter
|
@Getter
|
||||||
private final String uid;
|
private final String uid;
|
||||||
private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "Connection.java executor-service"));
|
private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "Connection.java executor-service"));
|
||||||
|
private final ExecutorService connectionThreadPool = Executors.newFixedThreadPool(MAX_CONNECTION_THREADS);
|
||||||
|
|
||||||
// holder of state shared between InputHandler and Connection
|
// holder of state shared between InputHandler and Connection
|
||||||
@Getter
|
@Getter
|
||||||
private final Statistic statistic;
|
private final Statistic statistic;
|
||||||
@ -429,12 +432,18 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
// Only receive non - CloseConnectionMessage network_messages
|
// Only receive non - CloseConnectionMessage network_messages
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
|
||||||
checkArgument(connection.equals(this));
|
Connection that = this;
|
||||||
if (networkEnvelope instanceof BundleOfEnvelopes) {
|
connectionThreadPool.submit(new Runnable() {
|
||||||
onBundleOfEnvelopes((BundleOfEnvelopes) networkEnvelope, connection);
|
@Override
|
||||||
} else {
|
public void run() {
|
||||||
UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection)));
|
checkArgument(connection.equals(that));
|
||||||
}
|
if (networkEnvelope instanceof BundleOfEnvelopes) {
|
||||||
|
onBundleOfEnvelopes((BundleOfEnvelopes) networkEnvelope, connection);
|
||||||
|
} else {
|
||||||
|
messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) {
|
private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) {
|
||||||
@ -466,8 +475,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
envelopesToProcess.add(networkEnvelope);
|
envelopesToProcess.add(networkEnvelope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
envelopesToProcess.forEach(envelope -> UserThread.execute(() ->
|
envelopesToProcess.forEach(envelope ->
|
||||||
messageListeners.forEach(listener -> listener.onMessage(envelope, connection))));
|
messageListeners.forEach(listener -> listener.onMessage(envelope, connection)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -793,11 +802,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throttle inbound network_messages
|
// Throttle inbound network messages
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
long elapsed = now - lastReadTimeStamp;
|
long elapsed = now - lastReadTimeStamp;
|
||||||
if (elapsed < 10) {
|
if (elapsed < 10) {
|
||||||
log.debug("We got 2 network_messages received in less than 10 ms. We set the thread to sleep " +
|
log.info("We got 2 network messages received in less than 10 ms. We set the thread to sleep " +
|
||||||
"for 20 ms to avoid getting flooded by our peer. lastReadTimeStamp={}, now={}, elapsed={}",
|
"for 20 ms to avoid getting flooded by our peer. lastReadTimeStamp={}, now={}, elapsed={}",
|
||||||
lastReadTimeStamp, now, elapsed);
|
lastReadTimeStamp, now, elapsed);
|
||||||
Thread.sleep(20);
|
Thread.sleep(20);
|
||||||
|
@ -1497,36 +1497,38 @@ message Trade {
|
|||||||
enum State {
|
enum State {
|
||||||
PB_ERROR_STATE = 0;
|
PB_ERROR_STATE = 0;
|
||||||
PREPARATION = 1;
|
PREPARATION = 1;
|
||||||
TAKER_PUBLISHED_TAKER_FEE_TX = 2;
|
CONTRACT_SIGNATURE_REQUESTED = 2;
|
||||||
MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 3;
|
CONTRACT_SIGNED = 3;
|
||||||
MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 4;
|
TAKER_PUBLISHED_TAKER_FEE_TX = 4;
|
||||||
MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 5;
|
MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 5;
|
||||||
MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 6;
|
MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 6;
|
||||||
TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 7;
|
MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 7;
|
||||||
TAKER_PUBLISHED_DEPOSIT_TX = 8;
|
MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 8;
|
||||||
TAKER_SAW_DEPOSIT_TX_IN_NETWORK = 9;
|
TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 9;
|
||||||
TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 10;
|
TAKER_PUBLISHED_DEPOSIT_TX = 10;
|
||||||
TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 11;
|
TAKER_SAW_DEPOSIT_TX_IN_NETWORK = 11;
|
||||||
TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 12;
|
TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 12;
|
||||||
TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 13;
|
TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 13;
|
||||||
MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 14;
|
TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 14;
|
||||||
MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 15;
|
TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 15;
|
||||||
DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 16;
|
MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 16;
|
||||||
BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 17;
|
MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 17;
|
||||||
BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 18;
|
DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 18;
|
||||||
BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 19;
|
BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 19;
|
||||||
BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 20;
|
BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 20;
|
||||||
BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 21;
|
BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 21;
|
||||||
SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 22;
|
BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 22;
|
||||||
SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 23;
|
BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 23;
|
||||||
SELLER_PUBLISHED_PAYOUT_TX = 24;
|
SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 24;
|
||||||
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 25;
|
SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 25;
|
||||||
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 26;
|
SELLER_PUBLISHED_PAYOUT_TX = 26;
|
||||||
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 27;
|
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 27;
|
||||||
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 28;
|
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 28;
|
||||||
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 29;
|
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 29;
|
||||||
BUYER_SAW_PAYOUT_TX_IN_NETWORK = 30;
|
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 30;
|
||||||
WITHDRAW_COMPLETED = 31;
|
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 31;
|
||||||
|
BUYER_SAW_PAYOUT_TX_IN_NETWORK = 32;
|
||||||
|
WITHDRAW_COMPLETED = 33;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Phase {
|
enum Phase {
|
||||||
@ -1654,9 +1656,8 @@ message ProcessModel {
|
|||||||
NodeAddress temp_trading_peer_node_address = 1006;
|
NodeAddress temp_trading_peer_node_address = 1006;
|
||||||
string prepared_multisig_hex = 1007;
|
string prepared_multisig_hex = 1007;
|
||||||
string made_multisig_hex = 1008;
|
string made_multisig_hex = 1008;
|
||||||
bool multisig_setup_complete = 1009;
|
string multisig_address = 1009;
|
||||||
bool maker_ready_to_fund_multisig = 1010;
|
bool multisig_setup_complete = 1010; // TODO: remove this field
|
||||||
bool multisig_deposit_initiated = 1011;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message TradingPeer {
|
message TradingPeer {
|
||||||
|
Loading…
Reference in New Issue
Block a user