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:
woodser 2022-03-31 08:17:58 -04:00
parent fdddc87477
commit bb95b4b1d6
82 changed files with 2786 additions and 2338 deletions

View File

@ -14,8 +14,13 @@ localnet:
haveno:
./gradlew build
haveno-apps: # quick build desktop and daemon apps without tests, etc
./gradlew :core:compileJava :desktop:build
# build haveno without tests
no-tests:
./gradlew build -x test
# quick build desktop and daemon apps without tests
haveno-apps:
./gradlew :core:compileJava :desktop:build -x test
deploy:
# create a new screen session named 'localnet'

View File

@ -42,43 +42,61 @@ public abstract class PersistableList<T extends PersistablePayload> implements P
}
public void setAll(Collection<T> collection) {
synchronized (this.list) {
this.list.clear();
this.list.addAll(collection);
}
}
public boolean add(T item) {
synchronized (list) {
if (!list.contains(item)) {
list.add(item);
return true;
}
return false;
}
}
public boolean remove(T item) {
synchronized (list) {
return list.remove(item);
}
}
public Stream<T> stream() {
synchronized (list) {
return list.stream();
}
}
public int size() {
synchronized (list) {
return list.size();
}
}
public boolean contains(T item) {
synchronized (list) {
return list.contains(item);
}
}
public boolean isEmpty() {
synchronized (list) {
return list.isEmpty();
}
}
public void forEach(Consumer<? super T> action) {
synchronized (list) {
list.forEach(action);
}
}
public void clear() {
synchronized (list) {
list.clear();
}
}
}

View File

@ -66,11 +66,14 @@ public abstract class Task<T extends Model> {
}
protected void failed(Throwable t) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
errorMessage = sw.toString();
log.error(t.getMessage(), t);
// // append stacktrace to error message (only for development)
// StringWriter sw = new StringWriter();
// PrintWriter pw = new PrintWriter(sw);
// t.printStackTrace(pw);
// errorMessage = sw.toString();
errorMessage = t.getMessage() + " (task " + getClass().getSimpleName() + ")";
log.error(errorMessage, t);
taskHandler.handleErrorMessage(errorMessage);
}

View File

@ -434,7 +434,8 @@ public class CoreApi {
double buyerSecurityDeposit,
long triggerPrice,
String paymentAccountId,
Consumer<Offer> resultHandler) {
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreOffersService.createAndPlaceOffer(currencyCode,
directionAsString,
priceAsString,
@ -445,7 +446,8 @@ public class CoreApi {
buyerSecurityDeposit,
triggerPrice,
paymentAccountId,
resultHandler);
resultHandler,
errorMessageHandler);
}
public Offer editOffer(String offerId,

View File

@ -85,9 +85,10 @@ public class CoreDisputesService {
}
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)));
synchronized (trade) {
Offer offer = trade.getOffer();
if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId));
@ -104,9 +105,14 @@ public class CoreDisputesService {
String updatedMultisigHex = multisigWallet.getMultisigHex();
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
tradeManager.requestPersistence();
// close multisig wallet
xmrWalletService.closeMultisigWallet(trade.getId());
}
}
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
synchronized (trade) {
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
@ -142,16 +148,18 @@ public class CoreDisputesService {
return dispute;
}
}
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
try {
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
.filter(d -> tradeId.equals(d.getTradeId()))
.findFirst();
Dispute dispute;
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (tradeManager.getTrade(tradeId)) {
var closeDate = new Date();
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
var contract = dispute.getContract();
@ -192,6 +200,7 @@ public class CoreDisputesService {
}
arbitrationManager.requestPersistence();
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
@ -245,8 +254,8 @@ public class CoreDisputesService {
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
if (!dispute.isMediationDispute()) {
try {
synchronized (tradeManager.getTrade(dispute.getTradeId())) {
System.out.println(disputeResult);
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
@ -256,6 +265,9 @@ public class CoreDisputesService {
// boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher();
// if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx");
// open multisig wallet
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
// 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
boolean isOpener = dispute.isOpener();
System.out.println("Is dispute opener: " + isOpener);
@ -268,6 +280,10 @@ public class CoreDisputesService {
// send arbitrator's updated multisig hex with dispute result
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex());
// close multisig wallet
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
}
} catch (AddressFormatException e2) {
log.error("Error at close dispute", e2);
return;

View File

@ -32,7 +32,7 @@ import bisq.core.payment.PaymentAccount;
import bisq.core.user.User;
import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.utils.Fiat;
@ -175,7 +175,10 @@ class CoreOffersService {
List<String> allKeyImages = new ArrayList<String>();
for (Offer offer : offers) {
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) {
if (unreservedOffers.contains(offer)) continue;
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,
long triggerPrice,
String paymentAccountId,
Consumer<Offer> resultHandler) {
Consumer<Offer> resultHandler,
ErrorMessageHandler errorMessageHandler) {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
@ -252,7 +259,8 @@ class CoreOffersService {
buyerSecurityDeposit,
triggerPrice,
useSavingsWallet,
transaction -> resultHandler.accept(offer));
transaction -> resultHandler.accept(offer),
errorMessageHandler);
}
// Edit a placed offer.
@ -303,16 +311,14 @@ class CoreOffersService {
double buyerSecurityDeposit,
long triggerPrice,
boolean useSavingsWallet,
Consumer<Transaction> resultHandler) {
Consumer<Transaction> resultHandler,
ErrorMessageHandler errorMessageHandler) {
openOfferManager.placeOffer(offer,
buyerSecurityDeposit,
useSavingsWallet,
triggerPrice,
resultHandler::accept,
log::error);
if (offer.getErrorMessage() != null)
throw new IllegalStateException(offer.getErrorMessage());
errorMessageHandler);
}
private boolean offerMatchesDirectionAndCurrency(Offer offer,

View File

@ -100,7 +100,7 @@ class CorePaymentAccountsService {
// Crypto Currency Accounts
PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
synchronized PaymentAccount createCryptoCurrencyPaymentAccount(String accountName,
String currencyCode,
String address,
boolean tradeInstant) {

View File

@ -103,16 +103,24 @@ class CoreTradesService {
throw new IllegalArgumentException(format("payment account with id '%s' not found", paymentAccountId));
var useSavingsWallet = true;
//noinspection ConstantConditions
// synchronize access to take offer model // TODO (woodser): to avoid synchronizing, don't use stateful model
Coin txFeeFromFeeService; // TODO (woodser): remove this and other unused fields
Coin takerFee;
Coin fundsNeededForTrade;
synchronized (takeOfferModel) {
takeOfferModel.initModel(offer, paymentAccount, useSavingsWallet);
log.info("Initiating take {} offer, {}",
offer.isBuyOffer() ? "buy" : "sell",
takeOfferModel);
//noinspection ConstantConditions
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(),
takeOfferModel.getTxFeeFromFeeService(),
takeOfferModel.getTakerFee(),
takeOfferModel.getFundsNeededForTrade(),
txFeeFromFeeService,
takerFee,
fundsNeededForTrade,
offer,
paymentAccountId,
useSavingsWallet,
@ -225,7 +233,7 @@ class CoreTradesService {
}
private Optional<Trade> getOpenTrade(String tradeId) {
return tradeManager.getTradeById(tradeId);
return tradeManager.getOpenTrade(tradeId);
}
private Optional<Trade> getClosedTrade(String tradeId) {
@ -236,14 +244,14 @@ class CoreTradesService {
List<Trade> getTrades() {
coreWalletsService.verifyWalletsAreAvailable();
coreWalletsService.verifyEncryptedWalletIsUnlocked();
List<Trade> trades = new ArrayList<Trade>(tradeManager.getTrades());
List<Trade> trades = new ArrayList<Trade>(tradeManager.getOpenTrades());
trades.addAll(closedTradableManager.getClosedTrades());
return trades;
}
List<ChatMessage> getChatMessages(String tradeId) {
Trade trade;
var tradeOptional = tradeManager.getTradeById(tradeId);
var tradeOptional = tradeManager.getOpenTrade(tradeId);
if (tradeOptional.isPresent()) trade = tradeOptional.get();
else throw new IllegalStateException(format("trade with id '%s' not found", tradeId));
boolean isMaker = tradeManager.isMyOffer(trade.getOffer());
@ -253,7 +261,7 @@ class CoreTradesService {
void sendChatMessage(String tradeId, String message) {
Trade trade;
var tradeOptional = tradeManager.getTradeById(tradeId);
var tradeOptional = tradeManager.getOpenTrade(tradeId);
if (tradeOptional.isPresent()) trade = tradeOptional.get();
else throw new IllegalStateException(format("trade with id '%s' not found", tradeId));
boolean isMaker = tradeManager.isMyOffer(trade.getOffer());

View File

@ -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) {
if (currencyCode == null || currencyCode.isEmpty())
return;
if (!currencyCode.equalsIgnoreCase("BTC"))
if (!currencyCode.equalsIgnoreCase("BTC") && !currencyCode.equalsIgnoreCase("XMR"))
throw new IllegalStateException(format("wallet does not support %s", currencyCode));
}

View File

@ -94,7 +94,7 @@ public class Balances {
private void updatedBalances() {
// 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();
updateLockedBalance();
updateReservedOfferBalance();

View File

@ -46,15 +46,20 @@ public class MoneroWalletRpcManager {
* @return a client connected to the monero-wallet-rpc instance
*/
public MoneroWalletRpc startInstance(List<String> cmd) {
try {
// register given port
if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) {
int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1;
int port = Integer.parseInt(cmd.get(portArgumentPosition));
synchronized (registeredPorts) {
if (registeredPorts.containsKey(port)) throw new RuntimeException("Port " + port + " is already registered");
registeredPorts.put(port, null);
}
MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process
synchronized (registeredPorts) {
registeredPorts.put(port, walletRpc);
}
return walletRpc;
}
@ -70,7 +75,9 @@ public class MoneroWalletRpcManager {
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) {
@ -93,21 +100,31 @@ public class MoneroWalletRpcManager {
* @param save specifies if the wallet should be saved before closing
*/
public void stopInstance(MoneroWalletRpc walletRpc, boolean save) {
// unregister port
synchronized (registeredPorts) {
boolean found = false;
for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) {
if (walletRpc == entry.getValue()) {
walletRpc.close(save);
walletRpc.stopProcess();
found = true;
try { unregisterPort(entry.getKey()); }
catch (Exception e) { throw new MoneroError(e); }
try {
unregisterPort(entry.getKey());
} catch (Exception e) {
throw new MoneroError(e);
}
break;
}
}
if (!found) throw new RuntimeException("MoneroWalletRpc instance not associated with port");
if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port");
}
// close wallet and stop process
walletRpc.close(save);
walletRpc.stopProcess();
}
private int registerPort() throws IOException {
synchronized (registeredPorts) {
// register next consecutive port
if (startPort != null) {
@ -119,15 +136,23 @@ public class MoneroWalletRpcManager {
// register auto-assigned port
else {
ServerSocket socket = new ServerSocket(0); // use socket to get available port
int port = socket.getLocalPort();
socket.close();
int port = getLocalPort();
registeredPorts.put(port, null);
return port;
}
}
}
private void unregisterPort(int 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;
}
}

View File

@ -63,7 +63,7 @@ public class XmrWalletService {
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_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 CoreMoneroConnectionsService connectionsService;
@ -154,62 +154,58 @@ public class XmrWalletService {
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
}
public boolean walletExists(String walletName) {
String path = walletDir.toString() + File.separator + walletName;
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?
public boolean multisigWalletExists(String tradeId) {
return walletExists("xmr_multisig_trade_" + tradeId);
}
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
public synchronized MoneroWallet createMultisigWallet(String tradeId) {
log.info("{}.createMultisigWallet({})", getClass(), tradeId);
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
String path = "xmr_multisig_trade_" + tradeId;
MoneroWallet multisigWallet = null;
multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port
multisigWallets.put(tradeId, multisigWallet);
multisigWallet.startSyncing(5000l);
public MoneroWallet createMultisigWallet(String tradeId) {
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
Trade trade = tradeManager.getOpenTrade(tradeId).get();
synchronized (trade) {
if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId());
String path = "xmr_multisig_trade_" + trade.getId();
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); // auto-assign port
multisigWallets.put(trade.getId(), multisigWallet);
return multisigWallet;
}
}
public MoneroWallet getMultisigWallet(String tradeId) { // TODO (woodser): synchronize per wallet id
log.info("{}.getMultisigWallet({})", getClass(), tradeId);
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
String path = "xmr_multisig_trade_" + tradeId;
if (!walletExists(path)) return null;
// TODO (woodser): provide progress notifications during open?
public MoneroWallet getMultisigWallet(String tradeId) {
log.info("{}.getMultisigWallet({})", getClass().getSimpleName(), tradeId);
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
if (multisigWallets.containsKey(trade.getId())) return multisigWallets.get(trade.getId());
String path = "xmr_multisig_trade_" + trade.getId();
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(tradeId, multisigWallet);
multisigWallet.startSyncing(5000l); // TODO (woodser): use sync period from config. apps stall if too many multisig wallets and too short sync period
multisigWallets.put(trade.getId(), multisigWallet);
return multisigWallet;
}
}
public synchronized boolean deleteMultisigWallet(String tradeId) {
log.info("{}.deleteMultisigWallet({})", getClass(), tradeId);
public void closeMultisigWallet(String tradeId) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
Trade trade = tradeManager.getTrade(tradeId);
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;
try {
closeWallet(getMultisigWallet(tradeId), false);
} catch (Exception err) {
// multisig wallet may not be open
}
if (multisigWallets.containsKey(trade.getId())) closeMultisigWallet(tradeId);
deleteWallet(walletName);
multisigWallets.remove(tradeId);
return true;
}
}
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
try {
@ -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() {
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
@ -288,7 +289,7 @@ public class XmrWalletService {
// create wallet
try {
walletRpc.createWallet(config);
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
walletRpc.startSyncing(MONERO_WALLET_SYNC_PERIOD);
return walletRpc;
} catch (Exception e) {
e.printStackTrace();
@ -305,7 +306,7 @@ public class XmrWalletService {
// open wallet
try {
walletRpc.openWallet(config);
walletRpc.startSyncing(MONERO_WALLET_SYNC_RATE);
walletRpc.startSyncing(MONERO_WALLET_SYNC_PERIOD);
return walletRpc;
} catch (Exception e) {
e.printStackTrace();
@ -370,7 +371,7 @@ public class XmrWalletService {
}
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()));
pool.submit(new Runnable() {
@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() {
// collect wallets to shutdown

View File

@ -429,7 +429,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
errorMessageHandler
);
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
}
@ -567,13 +569,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
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);
openOffer.setState(OpenOffer.State.CANCELED);
openOffers.remove(openOffer);
closedTradableManager.add(openOffer);
log.info("onRemoved offerId={}", offer.getId());
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
requestPersistence();
resultHandler.handleResult();
}
@ -727,11 +729,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
peer, response.getOfferId(), response.getUid());
// get previously created protocol
PlaceOfferProtocol protocol = placeOfferProtocols.get(response.getOfferId());
PlaceOfferProtocol protocol;
synchronized (placeOfferProtocols) {
protocol = placeOfferProtocols.get(response.getOfferId());
if (protocol == null) {
log.warn("No place offer protocol created for offer " + response.getOfferId());
return;
}
}
// handle response
protocol.handleSignOfferResponse(response, peer);

View File

@ -114,7 +114,7 @@ public class OfferAvailabilityProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
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());
stopTimeout();

View File

@ -45,20 +45,18 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
try {
runInterceptHook();
// synchronize on wallet to reserve key images
synchronized (model.getXmrWalletService().getWallet()) {
// create transaction to reserve trade
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
MoneroTxWallet reserveTx = TradeUtils.createReserveTx(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount);
MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount);
// freeze reserved outputs
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
// collect reserved key images // TODO (woodser): switch to proof of reserve?
List<String> reservedKeyImages = new ArrayList<String>();
MoneroWallet wallet = model.getXmrWalletService().getWallet();
for (MoneroOutput input : reserveTx.getInputs()) {
reservedKeyImages.add(input.getKeyImage().getHex());
wallet.freezeOutput(input.getKeyImage().getHex());
}
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save offer state
// TODO (woodser): persist
@ -66,6 +64,7 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field
complete();
}
} catch (Throwable t) {
offer.setErrorMessage("An error occurred.\n" +
"Error message:\n"

View File

@ -84,6 +84,7 @@ public class MakerSendsSignOfferRequest extends Task<PlaceOfferModel> {
if (!sender.equals(arbitrator.getNodeAddress())) return;
AckMessage ackMessage = (AckMessage) decryptedMessageWithPubKey.getNetworkEnvelope();
if (!ackMessage.getSourceMsgClassName().equals(SignOfferRequest.class.getSimpleName())) return;
if (!ackMessage.getSourceUid().equals(request.getUid())) return;
if (ackMessage.isSuccess()) {
offer.setState(Offer.State.OFFER_FEE_RESERVED);
model.getP2PService().removeDecryptedDirectMessageListener(this);

View File

@ -114,7 +114,7 @@ public class TakeOfferModel implements Model {
this.clearModel();
this.offer = offer;
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();
this.useSavingsWallet = useSavingsWallet;

View File

@ -17,6 +17,7 @@
package bisq.core.presentation;
import bisq.common.UserThread;
import bisq.core.trade.TradeManager;
import javax.inject.Inject;
@ -38,10 +39,12 @@ public class TradePresentation {
public TradePresentation(TradeManager tradeManager) {
tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> {
long numPendingTrades = (long) newValue;
UserThread.execute(() -> {
if (numPendingTrades > 0)
this.numPendingTrades.set(String.valueOf(numPendingTrades));
showPendingTradesNotification.set(numPendingTrades > 0);
});
});
}
}

View File

@ -128,11 +128,6 @@ public class FeeService {
}
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();
// We all requests only each 2 minutes
if (now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) {

View File

@ -51,6 +51,7 @@ public abstract class SupportManager {
protected final CoreMoneroConnectionsService connectionService;
protected final CoreNotificationService notificationService;
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> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
protected final MailboxMessageService mailboxMessageService;
@ -69,16 +70,24 @@ public abstract class SupportManager {
// We get first the message handler called then the onBootstrapped
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
// already stored
if (isReady()) applyDirectMessage(decryptedMessageWithPubKey);
else {
synchronized (lock) {
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
tryApplyMessages();
}
}
});
mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
// As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was
// already stored
decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey);
if (isReady()) applyMailboxMessage(decryptedMessageWithPubKey);
else {
synchronized (lock) {
// 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) {
final String tradeId = chatMessage.getTradeId();
final String uid = chatMessage.getUid();
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
boolean channelOpen = channelOpen(chatMessage);
if (!channelOpen) {
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() {
return allServicesInitialized &&
p2PService.isBootstrapped() &&
@ -306,17 +321,24 @@ public abstract class SupportManager {
///////////////////////////////////////////////////////////////////////////////////////////
private void applyMessages() {
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
synchronized (lock) {
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
decryptedDirectMessageWithPubKeys.clear();
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
decryptedMailboxMessageWithPubKeys.clear();
}
}
private void applyDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
if (networkEnvelope instanceof SupportMessage) {
onSupportMessage((SupportMessage) networkEnvelope);
} else if (networkEnvelope instanceof AckMessage) {
onAckMessage((AckMessage) networkEnvelope);
}
});
decryptedDirectMessageWithPubKeys.clear();
}
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
private void applyMailboxMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName());
if (networkEnvelope instanceof SupportMessage) {
@ -328,7 +350,5 @@ public abstract class SupportManager {
onAckMessage(ackMessage);
mailboxMessageService.removeMailboxMsg(ackMessage);
}
});
decryptedMailboxMessageWithPubKeys.clear();
}
}

View File

@ -23,7 +23,7 @@ import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.SupportType;
import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Contract;
import bisq.common.UserThread;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil;
import bisq.common.proto.network.NetworkPayload;
@ -365,7 +365,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
public void setState(Dispute.State disputeState) {
this.disputeState = disputeState;
this.isClosedProperty.set(disputeState == State.CLOSED);
UserThread.execute(() -> this.isClosedProperty.set(disputeState == State.CLOSED));
}
public void setDisputeResult(DisputeResult disputeResult) {

View File

@ -302,13 +302,22 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
return;
}
String errorMessage = null;
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
dispute.setSupportType(openNewDisputeMessage.getSupportType());
// 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
Trade trade = tradeManager.getTrade(dispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
return;
}
synchronized (trade) {
String errorMessage = null;
Contract contract = dispute.getContract();
addPriceInfoMessage(dispute, 0);
@ -318,9 +327,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update arbitrator's multisig wallet
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex()));
System.out.println("Arbitrator multisig wallet updated on new dispute message, current txs:");
System.out.println(multisigWallet.getTxs());
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
// close multisig wallet
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
@ -332,9 +344,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
dispute.getTradeId());
}
} else {
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
errorMessage = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId();
log.warn(errorMessage);
}
}
} else {
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
log.error(errorMessage);
@ -364,6 +377,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
requestPersistence();
}
}
// Not-dispute-requester receives that msg from dispute agent
protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
@ -375,14 +389,17 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
String errorMessage = null;
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()) {
return;
}
Trade trade = optionalTrade.get();
synchronized (trade) {
if (!isAgent(dispute)) {
synchronized (disputeList) {
if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) {
@ -399,6 +416,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
log.warn(errorMessage);
}
}
} else {
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
log.error(errorMessage);
@ -414,6 +432,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
requestPersistence();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -425,14 +444,17 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
String updatedMultisigHex,
ResultHandler resultHandler,
FaultHandler faultHandler) {
log.info("{}.sendOpenNewDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
T disputeList = getDisputeList();
if (disputeList == null) {
log.warn("disputes is null");
return;
}
synchronized (disputeList) {
if (disputeList.contains(dispute)) {
String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
log.warn(msg);
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
return;
@ -526,6 +548,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
log.warn(msg);
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
}
}
requestPersistence();
}
@ -533,6 +557,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
Contract contractFromOpener,
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
// 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
@ -604,7 +629,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
addPriceInfoMessage(dispute, 0);
synchronized (disputeList) {
disputeList.add(dispute);
}
// We mirrored dispute already!
Contract contract = dispute.getContract();
@ -826,9 +853,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
}
public Optional<Trade> findTrade(Dispute dispute) {
Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId());
Optional<Trade> retVal = tradeManager.getOpenTrade(dispute.getTradeId());
if (!retVal.isPresent()) {
retVal = closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(dispute.getTradeId())).findFirst();
retVal = tradeManager.getClosedTrade(dispute.getTradeId());
}
return retVal;
}

View File

@ -120,8 +120,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
@Override
public void onSupportMessage(SupportMessage message) {
if (canProcessMessage(message)) {
log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
log.info("Received {} from {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
if (message instanceof OpenNewDisputeMessage) {
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
@ -195,9 +195,10 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
ChatMessage chatMessage = disputeResult.getChatMessage();
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();
log.info("{}.onDisputeResultMessage() for trade {}", getClass().getSimpleName(), disputeResult.getTradeId());
Optional<Dispute> disputeOptional = findDispute(disputeResult);
String uid = disputeResultMessage.getUid();
if (!disputeOptional.isPresent()) {
@ -239,7 +240,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
String errorMessage = null;
boolean success = true;
boolean requestUpdatedPayoutTx = false;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
Contract contract = dispute.getContract();
try {
// 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 {
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
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);
// 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();
@ -344,6 +351,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
String uid = peerPublishedDisputePayoutTxMessage.getUid();
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
// get dispute and trade
Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) {
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
@ -357,8 +369,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
return;
}
Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract();
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
@ -366,11 +378,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
cleanupRetryMap(uid);
// update multisig wallet
// TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
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());
if (multisigWallet != null) {
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);
}
@ -383,10 +395,14 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
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
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName());
String tradeId = request.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
Dispute dispute = findDispute(request.getDispute().getTradeId(), request.getDispute().getTraderId()).get();
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
Contract contract = dispute.getContract();
@ -429,6 +445,9 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
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(
@ -456,12 +475,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
);
}
}
// Dispute opener's peer receives updated payout tx after providing updated multisig hex (if co-signer)
private void onArbitratorPayoutTxResponse(ArbitratorPayoutTxResponse response) {
log.info("{}.onArbitratorPayoutTxResponse()", getClass().getSimpleName());
// gather and verify trade info // TODO (woodser): verify response is from arbitrator, etc
String tradeId = response.getTradeId();
Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
// verify and sign dispute payout tx
MoneroTxSet signedPayoutTx = traderSignsDisputePayoutTx(tradeId, response.getArbitratorSignedPayoutTxHex());
@ -469,11 +492,77 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// process fully signed payout tx (publish, notify peer, etc)
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) {
// gather trade info
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
Optional<Dispute> disputeOptional = findDispute(tradeId);
if (!disputeOptional.isPresent()) {
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();
Contract contract = dispute.getContract();
Trade trade = tradeManager.getTradeById(tradeId).get();
Trade trade = tradeManager.getOpenTrade(tradeId).get();
// 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
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
@ -539,7 +630,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
private void updateTradeOrOpenOfferManager(String tradeId) {
// 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);
} else {
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);
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;
}
}

View File

@ -134,7 +134,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
@Override
public void cleanupDisputes() {
disputeListService.cleanupDisputes(tradeId -> {
tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null)
tradeManager.getOpenTrade(tradeId).filter(trade -> trade.getPayoutTx() != null)
.ifPresent(trade -> {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
});
@ -197,7 +197,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
dispute.setDisputeResult(disputeResult);
Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(tradeId);
if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get();
if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED ||

View File

@ -200,7 +200,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
dispute.setDisputeResult(disputeResult);
Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(tradeId);
if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get();
if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED ||
@ -215,7 +215,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
// 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);
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);

View File

@ -83,7 +83,7 @@ public class TraderChatManager extends SupportManager {
@Override
public NodeAddress getPeerNodeAddress(ChatMessage message) {
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
if (trade.getContract() != null) {
return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get());
} else {
@ -94,7 +94,7 @@ public class TraderChatManager extends SupportManager {
@Override
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
if (trade.getContract() != null) {
return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get());
} else {
@ -112,12 +112,12 @@ public class TraderChatManager extends SupportManager {
@Override
public boolean channelOpen(ChatMessage message) {
return tradeManager.getTradeById(message.getTradeId()).isPresent();
return tradeManager.getOpenTrade(message.getTradeId()).isPresent();
}
@Override
public void addAndPersistChatMessage(ChatMessage message) {
tradeManager.getTradeById(message.getTradeId()).ifPresent(trade -> {
tradeManager.getOpenTrade(message.getTradeId()).ifPresent(trade -> {
ObservableList<ChatMessage> chatMessages = trade.getChatMessages();
if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) {
if (chatMessages.isEmpty()) {

View File

@ -54,11 +54,13 @@ public final class TradableList<T extends Tradable> extends PersistableListAsObs
@Override
public Message toProtoMessage() {
synchronized (getList()) {
return protobuf.PersistableEnvelope.newBuilder()
.setTradableList(protobuf.TradableList.newBuilder()
.addAllTradable(ProtoUtil.collectionToProto(getList(), protobuf.Tradable.class)))
.build();
}
}
public static TradableList<Tradable> fromProto(protobuf.TradableList proto,
CoreProtoResolver coreProtoResolver,

View File

@ -63,6 +63,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@ -82,8 +83,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.common.MoneroError;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxWallet;
import monero.wallet.model.MoneroWalletListener;
@ -99,9 +100,11 @@ public abstract class Trade implements Tradable, Model {
///////////////////////////////////////////////////////////////////////////////////////////
public enum State {
// #################### Phase PREPARATION
// #################### Phase INIT
// When trade protocol starts no funds are on stake
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
// taker perspective
@ -206,9 +209,9 @@ public abstract class Trade implements Tradable, Model {
public enum Phase {
INIT,
TAKER_FEE_PUBLISHED,
TAKER_FEE_PUBLISHED, // TODO (woodser): remove unused phases
DEPOSIT_PUBLISHED,
DEPOSIT_CONFIRMED,
DEPOSIT_CONFIRMED, // TODO (woodser): rename to or add DEPOSIT_UNLOCKED
FIAT_SENT,
FIAT_RECEIVED,
PAYOUT_PUBLISHED,
@ -463,8 +466,8 @@ public abstract class Trade implements Tradable, Model {
transient MoneroWalletListener depositTxListener;
transient Boolean makerDepositLocked; // null when unknown, true while locked, false when unlocked
transient Boolean takerDepositLocked;
transient private MoneroTxWallet makerDepositTx;
transient private MoneroTxWallet takerDepositTx;
transient private MoneroTx makerDepositTx;
transient private MoneroTx takerDepositTx;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialization
@ -696,23 +699,14 @@ public abstract class Trade implements Tradable, Model {
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() {
if (getMakerDepositTx() != null && getTakerDepositTx() != null) {
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(getId());
applyDepositTxs(multisigWallet.getTx(getMakerDepositTx().getHash()), multisigWallet.getTx(getTakerDepositTx().getHash()));
}
}
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() {
/**
* Listen for deposit transactions to unlock and then apply the transactions.
*
* TODO: adopt for general purpose scheduling
* 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());
// ignore if already listening
if (depositTxListener != null) {
@ -720,48 +714,86 @@ public abstract class Trade implements Tradable, Model {
return;
}
// create listener for deposit transactions
MoneroWallet multisigWallet = processModel.getXmrWalletService().getMultisigWallet(getId());
// get daemon and primary wallet
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
Long unlockHeight = null;
@Override
public void onOutputReceived(MoneroOutputWallet output) {
public void onNewBlock(long height) {
// ignore if no longer listening
if (depositTxListener == null) return;
// TODO (woodser): remove this
if (output.getTx().isConfirmed() && output.getTx().isLocked() && (processModel.getMaker().getDepositTxHash().equals(output.getTx().getHash()) || processModel.getTaker().getDepositTxHash().equals(output.getTx().getHash()))) {
System.out.println("Deposit output for tx " + output.getTx().getHash() + " is confirmed at height " + output.getTx().getHeight());
}
// ignore if before unlock height
if (unlockHeight != null && height < unlockHeight) return;
// update locked state
if (output.getTx().getHash().equals(processModel.getMaker().getDepositTxHash())) makerDepositLocked = output.getTx().isLocked();
else if (output.getTx().getHash().equals(processModel.getTaker().getDepositTxHash())) takerDepositLocked = output.getTx().isLocked();
// fetch txs from daemon
List<MoneroTx> txs = daemon.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()), true);
// deposit txs seen when both locked states seen
if (makerDepositLocked != null && takerDepositLocked != null) {
// ignore if deposit txs not seen
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);
}
// confirm trade and update ui when both deposits unlock
if (Boolean.FALSE.equals(makerDepositLocked) && Boolean.FALSE.equals(takerDepositLocked)) {
System.out.println("Multisig deposit txs unlocked!");
applyDepositTxs(multisigWallet.getTx(processModel.getMaker().getDepositTxHash()), multisigWallet.getTx(processModel.getTaker().getDepositTxHash()));
multisigWallet.removeListener(depositTxListener); // remove listener when notified
// compute unlock height
if (unlockHeight == null && txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) {
unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(0).getHeight()) + 9;
}
// 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
}
}
});
// register wallet listener
multisigWallet.addListener(depositTxListener);
havenoWallet.addListener(depositTxListener);
}
@Nullable
public MoneroTxWallet getTakerDepositTx() {
public MoneroTx getTakerDepositTx() {
String depositTxHash = getProcessModel().getTaker().getDepositTxHash();
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;
} catch (MoneroError e) {
log.error("Wallet is missing taker deposit tx " + depositTxHash);
@ -770,10 +802,10 @@ public abstract class Trade implements Tradable, Model {
}
@Nullable
public MoneroTxWallet getMakerDepositTx() {
public MoneroTx getMakerDepositTx() {
String depositTxHash = getProcessModel().getMaker().getDepositTxHash();
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;
} catch (MoneroError e) {
log.error("Wallet is missing maker deposit tx " + depositTxHash);
@ -1047,10 +1079,10 @@ public abstract class Trade implements Tradable, Model {
private long getTradeStartTime() {
long now = System.currentTimeMillis();
long startTime;
final MoneroTxWallet takerDepositTx = getTakerDepositTx();
final MoneroTxWallet makerDepositTx = getMakerDepositTx();
final MoneroTx takerDepositTx = getTakerDepositTx();
final MoneroTx makerDepositTx = getMakerDepositTx();
if (makerDepositTx != null && takerDepositTx != null && getTakeOfferDate() != null) {
if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) {
if (isDepositConfirmed()) {
final long tradeTime = getTakeOfferDate().getTime();
long maxHeight = Math.max(makerDepositTx.getHeight(), takerDepositTx.getHeight());
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();

View File

@ -118,7 +118,7 @@ import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.model.MoneroTxWallet;
import monero.daemon.model.MoneroTx;
public class TradeManager implements PersistedDataHost, DecryptedDirectMessageListener {
@ -346,7 +346,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void initPersistedTrade(Trade trade) {
initTradeAndProtocol(trade, getTradeProtocol(trade));
trade.updateDepositTxFromWallet(); // TODO (woodser): this re-opens all multisig wallets. only open active wallets
requestPersistence();
}
@ -405,7 +404,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
Trade trade;
Optional<Trade> tradeOptional = getTradeById(offer.getId());
Optional<Trade> tradeOptional = getOpenTrade(offer.getId());
if (tradeOptional.isPresent()) {
trade = tradeOptional.get();
@ -446,11 +445,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
initTradeAndProtocol(trade, getTradeProtocol(trade));
synchronized (tradableList) {
tradableList.add(trade);
}
}
((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);
});
@ -514,7 +515,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex());
trade.getSelf().setReserveTxKey(openOffer.getReserveTxKey());
trade.getSelf().setReserveTxKeyImages(offer.getOfferPayload().getReserveTxKeyImages());
synchronized (tradableList) {
tradableList.add(trade);
}
// notify on phase changes
// TODO (woodser): save subscription, bind on startup
@ -545,7 +548,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId());
return;
@ -564,7 +567,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId());
return;
@ -583,7 +586,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId());
return;
@ -602,7 +605,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId());
return;
@ -621,7 +624,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
Optional<Trade> tradeOptional = getTradeById(response.getTradeId());
Optional<Trade> tradeOptional = getOpenTrade(response.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + response.getTradeId());
return;
@ -640,7 +643,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
Optional<Trade> tradeOptional = getTradeById(request.getTradeId());
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId());
return;
@ -659,7 +662,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
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
Trade trade = tradeOptional.get();
getTradeProtocol(trade).handleUpdateMultisigRequest(request, peer, errorMessage -> {
@ -737,7 +740,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
if (prev != null) {
log.error("We had already an entry with uid {}", trade.getUid());
}
synchronized (tradableList) {
tradableList.add(trade);
}
initTradeAndProtocol(trade, tradeProtocol);
@ -753,7 +758,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
requestPersistence();
}
},
errorMessageHandler);
errorMessage -> {
log.warn("Taker error during check offer availability: " + errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
});
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
public void onTradeCompleted(Trade trade) {
removeTrade(trade);
closedTradableManager.add(trade);
removeTrade(trade);
// TODO The address entry should have been removed already. Check and if its the case remove that.
xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId());
@ -811,7 +819,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
///////////////////////////////////////////////////////////////////////////////////////////
public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) {
Optional<Trade> tradeOptional = getTradeById(tradeId);
Optional<Trade> tradeOptional = getOpenTrade(tradeId);
if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get();
trade.setDisputeState(disputeState);
@ -897,9 +905,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
.collect(Collectors.toSet()));
tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn()
.map(trade -> {
MoneroTxWallet makerDepositTx = trade.getMakerDepositTx();
MoneroTx makerDepositTx = trade.getMakerDepositTx();
if (makerDepositTx != null) {
if (makerDepositTx.isLocked()) {
if (!makerDepositTx.isConfirmed()) {
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx
} else {
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())));
}
MoneroTxWallet takerDepositTx = trade.getTakerDepositTx();
MoneroTx takerDepositTx = trade.getTakerDepositTx();
if (takerDepositTx != null) {
if (!takerDepositTx.isConfirmed()) {
tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId())));
@ -940,9 +948,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
initPersistedTrade(trade);
synchronized (tradableList) {
if (!tradableList.contains(trade)) {
tradableList.add(trade);
}
}
return true;
}
@ -967,8 +978,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
///////////////////////////////////////////////////////////////////////////////////////////
public ObservableList<Trade> getObservableList() {
synchronized (tradableList) {
return tradableList.getObservableList();
}
}
public BooleanProperty persistedTradesInitializedProperty() {
return persistedTradesInitialized;
@ -979,7 +992,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
public boolean wasOfferAlreadyUsedInTrade(String offerId) {
return getTradeById(offerId).isPresent() ||
return getOpenTrade(offerId).isPresent() ||
failedTradesManager.getTradeById(offerId).isPresent() ||
closedTradableManager.getTradableById(offerId).isPresent();
}
@ -992,38 +1005,59 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
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();
}
public List<Trade> getTrades() {
public List<Trade> getOpenTrades() {
return ImmutableList.copyOf(getObservableList().stream()
.filter(e -> e instanceof Trade)
.map(e -> e)
.collect(Collectors.toList()));
}
private void removeTrade(Trade trade) {
if (tradableList.remove(trade)) {
public Optional<Trade> getClosedTrade(String tradeId) {
return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst();
}
// unreserve taker trade key images
private void removeTrade(Trade trade) {
log.info("TradeManager.removeTrade()");
synchronized(tradableList) {
if (!tradableList.contains(trade)) return;
synchronized (trade) {
// unreserve trade key images
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) {
xmrWalletService.getWallet().thawOutput(keyImage);
}
}
// delete multisig wallet // TODO (woodser): don't delete multisig wallet until payout tx unlocked
if (xmrWalletService.multisigWalletExists(trade.getId())) xmrWalletService.deleteMultisigWallet(trade.getId());
else log.warn("Multisig wallet to delete for trade {} does not exist", trade.getId());
// unregister and persist
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
xmrWalletService.deleteMultisigWallet(trade.getId()); // TODO (woodser): don't delete multisig wallet until payout tx unlocked?
tradableList.remove(trade);
requestPersistence();
}
}
}
private void addTrade(Trade trade) {
synchronized(tradableList) {
if (tradableList.add(trade)) {
requestPersistence();
}
}
}
// TODO Remove once tradableList is refactored to a final field
// (part of the persistence refactor PR)

View File

@ -137,9 +137,9 @@ public class TradeUtils {
}
/**
* Create a transaction to reserve a trade. The deposit amount is returned
* to the sender's payout address. Additional funds are reserved to allow
* fluctuations in the mining fee.
* Create a transaction to reserve a trade and freeze its funds. The deposit
* amount is returned to the sender's payout address. Additional funds are
* reserved to allow fluctuations in the mining fee.
*
* @param xmrWalletService
* @param offerId
@ -147,7 +147,7 @@ public class TradeUtils {
* @param depositAmount
* @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
MoneroWallet wallet = xmrWalletService.getWallet();
@ -163,6 +163,11 @@ public class TradeUtils {
.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?
// freeze trade funds
for (MoneroOutput input : reserveTx.getInputs()) {
wallet.freezeOutput(input.getKeyImage().getHex());
}
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");
}
// verify the unlock height
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee
String feeAddress = TradeUtils.FEE_ADDRESS;
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);

View File

@ -8,14 +8,14 @@ import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.protocol.tasks.ApplyFilter;
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.ArbitratorProcessesReserveTx;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.core.trade.protocol.tasks.ProcessSignContractRequest;
import bisq.core.util.Validator;
import bisq.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import lombok.extern.slf4j.Slf4j;
@ -34,8 +34,10 @@ public class ArbitratorProtocol extends DisputeProtocol {
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer,
ErrorMessageHandler errorMessageHandler) {
synchronized (trade) {
this.errorMessageHandler = errorMessageHandler;
processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set
CountDownLatch latch = new CountDownLatch(1);
//processModel.setTempTradingPeerNodeAddress(peer);
expect(phase(Trade.Phase.INIT)
.with(message)
@ -47,21 +49,27 @@ public class ArbitratorProtocol extends DisputeProtocol {
ArbitratorSendsInitTradeAndMultisigRequests.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(peer, message, errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println("ArbitratorProtocol.handleInitMultisigRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(request)
.from(sender))
@ -69,21 +77,27 @@ public class ArbitratorProtocol extends DisputeProtocol {
ProcessInitMultisigRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(sender, request, errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println("ArbitratorProtocol.handleSignContractRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(message)
.from(sender))
@ -92,36 +106,46 @@ public class ArbitratorProtocol extends DisputeProtocol {
ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(sender, message, errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
System.out.println("ArbitratorProtocol.handleDepositRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(request)
.from(sender))
.setup(tasks(
ProcessDepositRequest.class)
ArbitratorProcessesDepositRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
stopTimeout();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(sender, request, errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -19,6 +19,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.BuyerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
import bisq.core.trade.messages.DepositResponse;
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.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
@Slf4j
public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol {
@ -74,7 +76,10 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer,
ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
synchronized (trade) {
this.errorMessageHandler = errorMessageHandler;
CountDownLatch latch = new CountDownLatch(1);
expect(phase(Trade.Phase.INIT)
.with(message)
.from(peer))
@ -85,21 +90,27 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
MakerSendsInitTradeRequestIfUnreserved.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(peer, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleInitMultisigRequest()");
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request); // TODO (woodser): synchronize access since concurrent requests processed
processModel.setTradeMessage(request);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(request)
.from(sender))
@ -108,21 +119,27 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
SendSignContractRequestAfterMultisig.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleSignContractRequest()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(message)
.from(sender))
@ -131,22 +148,29 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleSignContractResponse()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
expect(anyPhase(Trade.Phase.INIT)
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(message)
.from(sender))
.setup(tasks(
@ -154,22 +178,33 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
ProcessSignContractResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender);
});
}
}
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleDepositResponse()");
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), response);
processModel.setTradeMessage(response);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(response)
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
.setup(tasks(
@ -177,22 +212,29 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
ProcessDepositResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, response);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, response, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handlePaymentAccountPayloadRequest()");
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
processModel.setTradeMessage(request);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
.with(request)
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
.setup(tasks(
@ -201,15 +243,24 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
MakerRemovesOpenOffer.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
stopTimeout();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
});
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -21,6 +21,7 @@ package bisq.core.trade.protocol;
import bisq.core.offer.Offer;
import bisq.core.trade.BuyerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
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.SignContractResponse;
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.ProcessDepositResponse;
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.util.Validator;
import bisq.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
import static com.google.common.base.Preconditions.checkNotNull;
@ -83,40 +86,47 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction: Take offer
// Take offer
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): this implementation is duplicated with SellerAsTakerProtocol
// TODO (woodser): these methods are duplicated with SellerAsTakerProtocol due to single inheritance
@Override
public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) {
System.out.println("onTakeOffer()");
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) // TODO (woodser): app hangs if this pipeline fails. use .using() like below
TakerSendsInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> { },
errorMessageHandler))
.withTimeout(30))
() -> {
latch.countDown();
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// TakerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): these methods are duplicated with SellerAsTakerProtocol due to single inheritance
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println("BuyerAsTakerProtocol.handleInitMultisigRequest()");
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(request)
.from(sender))
@ -125,21 +135,27 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
SendSignContractRequestAfterMultisig.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handleSignContractRequest()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(message)
.from(sender))
@ -148,22 +164,29 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handleSignContractResponse()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
processModel.setTradeMessage(message);
expect(anyPhase(Trade.Phase.INIT)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(message)
.from(sender))
.setup(tasks(
@ -171,22 +194,34 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
ProcessSignContractResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state != State.CONTRACT_SIGNATURE_REQUESTED) return;
handleSignContractResponse(message, sender);
});
}
}
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handleDepositResponse()");
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), response);
processModel.setTradeMessage(response);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(response)
.from(sender))
.setup(tasks(
@ -194,22 +229,29 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
ProcessDepositResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, response);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, response, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handlePaymentAccountPayloadRequest()");
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
CountDownLatch latch = new CountDownLatch(1);
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
processModel.setTradeMessage(request);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) // TODO (woodser): rename to RECEIVED_DEPOSIT_TX_PUBLISHED_MSG
.with(request)
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
.setup(tasks(
@ -217,16 +259,25 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
ProcessPaymentAccountPayloadRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
stopTimeout();
handleTaskRunnerSuccess(sender, request);
tradeResultHandler.handleResult(trade); // trade is initialized
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
});
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -33,7 +33,7 @@ import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStar
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener;
import bisq.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
@ -127,6 +127,9 @@ public abstract class BuyerProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
System.out.println("BuyerProtocol.onPaymentStarted()");
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
System.out.println("BuyerProtocol.onPaymentStarted() has the lock!!!");
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
expect(phase(Trade.Phase.DEPOSIT_CONFIRMED)
.with(event)
@ -149,14 +152,18 @@ public abstract class BuyerProtocol extends DisputeProtocol {
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED))
.executeTasks();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Incoming message Payout tx
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
log.info("BuyerProtocol.handle(PayoutTxPublishedMessage)");
synchronized (trade) {
processModel.setTradeMessage(message);
processModel.setTempTradingPeerNodeAddress(peer);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
.with(message)
.from(peer))
@ -165,12 +172,16 @@ public abstract class BuyerProtocol extends DisputeProtocol {
BuyerProcessPayoutTxPublishedMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
latch.countDown();
handleTaskRunnerFault(peer, message, errorMessage);
})))
.executeTasks();
wait(latch);
}
}

View File

@ -92,6 +92,7 @@ public class FluentProtocol {
return this;
}
synchronized (tradeProtocol.trade) {
if (setup.getTimeoutSec() > 0) {
tradeProtocol.startTimeout(setup.getTimeoutSec());
}
@ -113,6 +114,7 @@ public class FluentProtocol {
taskRunner.run();
return this;
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -181,14 +181,11 @@ public class ProcessModel implements Model, PersistablePayload {
@Nullable
@Getter
@Setter
private boolean multisigSetupComplete;
private String multisigAddress;
@Nullable
@Getter
@Setter
private boolean makerReadyToFundMultisig; // TODO (woodser): remove
@Getter
@Setter
private boolean multisigDepositInitiated;
private boolean multisigSetupComplete; // TODO (woodser): redundant with multisigAddress existing, remove
@Nullable
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(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex));
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(makerReadyToFundMultisig).ifPresent(e -> builder.setMakerReadyToFundMultisig(makerReadyToFundMultisig));
Optional.ofNullable(multisigDepositInitiated).ifPresent(e -> builder.setMultisigSetupComplete(multisigDepositInitiated));
return builder.build();
}
@ -284,9 +280,8 @@ public class ProcessModel implements Model, PersistablePayload {
processModel.setBackupArbitrator(proto.hasBackupArbitrator() ? NodeAddress.fromProto(proto.getBackupArbitrator()) : null);
processModel.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
processModel.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress()));
processModel.setMultisigSetupComplete(proto.getMultisigSetupComplete());
processModel.setMakerReadyToFundMultisig(proto.getMakerReadyToFundMultisig());
processModel.setMultisigDepositInitiated(proto.getMultisigDepositInitiated());
String paymentStartedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentStartedMessageState());
MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString);

View File

@ -20,6 +20,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.SellerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.messages.DepositResponse;
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.util.Validator;
import bisq.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
@Slf4j
public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtocol {
@ -74,7 +76,10 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
public void handleInitTradeRequest(InitTradeRequest message,
NodeAddress peer,
ErrorMessageHandler errorMessageHandler) {
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
synchronized (trade) {
this.errorMessageHandler = errorMessageHandler;
CountDownLatch latch = new CountDownLatch(1);
expect(phase(Trade.Phase.INIT)
.with(message)
.from(peer))
@ -85,21 +90,27 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
MakerSendsInitTradeRequestIfUnreserved.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(peer, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleInitMultisigRequest()");
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request); // TODO (woodser): synchronize access since concurrent requests processed
processModel.setTradeMessage(request);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(request)
.from(sender))
@ -108,21 +119,27 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
SendSignContractRequestAfterMultisig.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleSignContractRequest()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(message)
.from(sender))
@ -131,22 +148,29 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleSignContractResponse()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message); // TODO (woodser): synchronize access since concurrent requests processed
expect(anyPhase(Trade.Phase.INIT)
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(message)
.from(sender))
.setup(tasks(
@ -154,22 +178,33 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
ProcessSignContractResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.CONTRACT_SIGNATURE_REQUESTED) handleSignContractResponse(message, sender);
});
}
}
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handleDepositResponse()");
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), response);
processModel.setTradeMessage(response);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(response)
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
.setup(tasks(
@ -177,22 +212,29 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
ProcessDepositResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, response);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, response, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
System.out.println("BuyerAsMakerProtocol.handlePaymentAccountPayloadRequest()");
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
processModel.setTradeMessage(request);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG)
.with(request)
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
.setup(tasks(
@ -201,15 +243,24 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
MakerRemovesOpenOffer.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
stopTimeout();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
});
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -21,6 +21,7 @@ package bisq.core.trade.protocol;
import bisq.core.offer.Offer;
import bisq.core.trade.SellerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
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.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
import static com.google.common.base.Preconditions.checkNotNull;
@ -76,15 +78,19 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
///////////////////////////////////////////////////////////////////////////////////////////
// User interaction: Take offer
// Take offer
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): these methods are duplicated with BuyerAsTakerProtocol due to single inheritance
@Override
public void onTakeOffer(TradeResultHandler tradeResultHandler,
ErrorMessageHandler errorMessageHandler) {
System.out.println("onTakeOffer()");
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()))
@ -93,23 +99,26 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
TakerReservesTradeFunds.class,
TakerSendsInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> { },
errorMessageHandler))
.withTimeout(30))
() -> {
latch.countDown();
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// TakerProtocol
///////////////////////////////////////////////////////////////////////////////////////////
// TODO (woodser): these methods are duplicated with BuyerAsTakerProtocol due to single inheritance
@Override
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println("BuyerAsTakerProtocol.handleInitMultisigRequest()");
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(request)
.from(sender))
@ -118,21 +127,27 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
SendSignContractRequestAfterMultisig.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handleSignContractRequest()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.INIT)
.with(message)
.from(sender))
@ -141,22 +156,29 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handleSignContractResponse()");
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
if (trade.getState() == State.CONTRACT_SIGNATURE_REQUESTED) {
processModel.setTradeMessage(message);
expect(anyPhase(Trade.Phase.INIT)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(message)
.from(sender))
.setup(tasks(
@ -164,22 +186,34 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
ProcessSignContractResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, message);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, message, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state != State.CONTRACT_SIGNATURE_REQUESTED) return;
handleSignContractResponse(message, sender);
});
}
}
}
@Override
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handleDepositResponse()");
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), response);
processModel.setTradeMessage(response);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED)
CountDownLatch latch = new CountDownLatch(1);
expect(state(Trade.State.CONTRACT_SIGNATURE_REQUESTED)
.with(response)
.from(sender))
.setup(tasks(
@ -187,22 +221,29 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
ProcessDepositResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
handleTaskRunnerSuccess(sender, response);
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, response, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
}
}
@Override
public void handlePaymentAccountPayloadRequest(PaymentAccountPayloadRequest request, NodeAddress sender) {
System.out.println("SellerAsTakerProtocol.handlePaymentAccountPayloadRequest()");
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountPayloadRequest()");
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), request);
CountDownLatch latch = new CountDownLatch(1);
if (trade.getState() == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) {
processModel.setTradeMessage(request);
expect(anyPhase(Trade.Phase.INIT, Trade.Phase.DEPOSIT_PUBLISHED) // TODO: only deposit_published should be expected
expect(state(Trade.State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) // TODO (woodser): rename to RECEIVED_DEPOSIT_TX_PUBLISHED_MSG
.with(request)
.from(sender)) // TODO (woodser): ensure this asserts sender == response.getSenderNodeAddress()
.setup(tasks(
@ -210,16 +251,25 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
ProcessPaymentAccountPayloadRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
stopTimeout();
handleTaskRunnerSuccess(sender, request);
tradeResultHandler.handleResult(trade); // trade is initialized
},
errorMessage -> {
errorMessageHandler.handleErrorMessage(errorMessage);
latch.countDown();
handleTaskRunnerFault(sender, request, errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
}))
.withTimeout(30))
.withTimeout(TRADE_TIMEOUT))
.executeTasks();
wait(latch);
} else {
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == State.MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG) handlePaymentAccountPayloadRequest(request, sender);
});
}
}
}

View File

@ -30,7 +30,7 @@ import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx;
import bisq.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler;
@ -77,11 +77,14 @@ public abstract class SellerProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
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
// that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received
// a mailbox message with CounterCurrencyTransferStartedMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
synchronized (trade) {
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED)
.with(message)
.from(peer)
@ -96,8 +99,17 @@ public abstract class SellerProtocol extends DisputeProtocol {
.setup(tasks(
SellerProcessCounterCurrencyTransferStartedMessage.class,
ApplyFilter.class,
getVerifyPeersFeePaymentClass()))
getVerifyPeersFeePaymentClass())
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
},
(errorMessage) -> {
latch.countDown();
})))
.executeTasks();
wait(latch);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -105,6 +117,9 @@ public abstract class SellerProtocol extends DisputeProtocol {
///////////////////////////////////////////////////////////////////////////////////////////
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
log.info("SellerProtocol.onPaymentReceived()");
synchronized (trade) {
CountDownLatch latch = new CountDownLatch(1);
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
.with(event)
@ -117,14 +132,18 @@ public abstract class SellerProtocol extends DisputeProtocol {
// SellerBroadcastPayoutTx.class,
SellerSendPayoutTxPublishedMessage.class)
.using(new TradeTaskRunner(trade, () -> {
latch.countDown();
resultHandler.handleResult();
handleTaskRunnerSuccess(event);
}, (errorMessage) -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT))
.executeTasks();
wait(latch);
}
}

View File

@ -49,6 +49,7 @@ import bisq.common.taskrunner.Task;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
@ -58,6 +59,8 @@ import javax.annotation.Nullable;
@Slf4j
public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener {
public static final int TRADE_TIMEOUT = 60;
protected final ProcessModel processModel;
protected final Trade trade;
private Timer timeoutTimer;
@ -213,23 +216,28 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// TODO (woodser): update to use fluent for consistency
public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
CountDownLatch latch = new CountDownLatch(1);
TradeTaskRunner taskRunner = new TradeTaskRunner(trade,
() -> {
stopTimeout();
latch.countDown();
handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest");
},
errorMessage -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(peer, message, errorMessage);
});
taskRunner.addTasks(
ProcessUpdateMultisigRequest.class
);
startTimeout(60); // TODO (woodser): what timeout to use? don't hardcode
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);
}
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
public final FluentProtocol.Setup tasks(Class<? extends Task<Trade>>... 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 {}",
ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getSourceUid());
} else {
log.warn("Received AckMessage with error state for {} from {} with tradeId {} and errorMessage={}",
ackMessage.getSourceMsgClassName(), peer, trade.getId(), ackMessage.getErrorMessage());
String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() +
" 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
///////////////////////////////////////////////////////////////////////////////////////////
protected void wait(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
protected void startTimeout(long timeoutSec) {
stopTimeout();
timeoutTimer = UserThread.runAfter(() -> {

View File

@ -40,10 +40,10 @@ import monero.daemon.MoneroDaemon;
import monero.wallet.MoneroWallet;
@Slf4j
public class ProcessDepositRequest extends TradeTask {
public class ArbitratorProcessesDepositRequest extends TradeTask {
@SuppressWarnings({"unused"})
public ProcessDepositRequest(TaskRunner taskHandler, Trade trade) {
public ArbitratorProcessesDepositRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -78,8 +78,7 @@ public class ProcessDepositRequest extends TradeTask {
boolean isFromTaker = request.getSenderNodeAddress().equals(trade.getTakerNodeAddress());
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()));
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId()); // TODO (woodser): only get, do not create
String depositAddress = multisigWallet.getPrimaryAddress();
String depositAddress = processModel.getMultisigAddress();
BigInteger tradeFee;
TradingPeer trader = trade.getTradingPeer(request.getSenderNodeAddress());
if (trader == processModel.getMaker()) tradeFee = ParsingUtils.coinToAtomicUnits(trade.getOffer().getMakerFee());
@ -103,9 +102,6 @@ public class ProcessDepositRequest extends TradeTask {
null,
false);
// sychronize to send only one response
synchronized(processModel) {
// set deposit info
trader.setDepositTxHex(request.getDepositTxHex());
trader.setDepositTxKey(request.getDepositTxKey());
@ -115,7 +111,7 @@ public class ProcessDepositRequest extends TradeTask {
if (processModel.getMaker().getDepositTxHex() != null && processModel.getTaker().getDepositTxHex() != null) {
// relay txs
daemon.submitTxHex(processModel.getMaker().getDepositTxHex());
daemon.submitTxHex(processModel.getMaker().getDepositTxHex()); // TODO (woodser): check that result is good. will need to release funds if one is submitted
daemon.submitTxHex(processModel.getTaker().getDepositTxHex());
// create deposit response
@ -131,7 +127,6 @@ public class ProcessDepositRequest extends TradeTask {
sendDepositResponse(trade.getMakerNodeAddress(), trade.getMakerPubKeyRing(), response);
sendDepositResponse(trade.getTakerNodeAddress(), trade.getTakerPubKeyRing(), response);
}
}
// TODO (woodser): request persistence?
complete();

View File

@ -44,9 +44,6 @@ import monero.wallet.MoneroWallet;
@Slf4j
public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
private boolean takerAck;
private boolean makerAck;
@SuppressWarnings({"unused"})
public ArbitratorSendsInitTradeAndMultisigRequests(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
@ -97,14 +94,15 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
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() {
@Override
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);
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
public void onArrived() {
log.info("{} arrived at maker: offerId={}; uid={}", makerRequest.getClass().getSimpleName(), makerRequest.getTradeId(), makerRequest.getUid());
complete();
}
@Override
public void onFault(String errorMessage) {
@ -172,8 +171,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
makerAck = true;
checkComplete();
}
@Override
public void onFault(String errorMessage) {
@ -194,8 +191,6 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
@Override
public void onArrived() {
log.info("{} arrived at peer: offerId={}; uid={}", initMultisigRequest.getClass().getSimpleName(), initMultisigRequest.getTradeId(), initMultisigRequest.getUid());
takerAck = true;
checkComplete();
}
@Override
public void onFault(String errorMessage) {
@ -206,8 +201,4 @@ public class ArbitratorSendsInitTradeAndMultisigRequests extends TradeTask {
}
);
}
private void checkComplete() {
if (makerAck && takerAck) complete();
}
}

View File

@ -60,6 +60,7 @@ public class ProcessDepositResponse extends TradeTask {
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
complete();
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
}
@Override
@ -69,8 +70,6 @@ public class ProcessDepositResponse extends TradeTask {
failed();
}
});
complete();
} catch (Throwable t) {
failed(t);
}

View File

@ -52,8 +52,6 @@ public class ProcessInitMultisigRequest extends TradeTask {
private boolean ack1 = false;
private boolean ack2 = false;
private boolean failed = false;
private static Object lock = new Object();
MoneroWallet multisigWallet;
@SuppressWarnings({"unused"})
@ -71,18 +69,10 @@ public class ProcessInitMultisigRequest extends TradeTask {
checkTradeId(processModel.getOfferId(), request);
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): run in separate thread to not block UI thread?
// TODO (woodser): validate message has expected sender in previous step
// synchronize access to wallet
synchronized (lock) {
// get peer multisig participant
TradingPeer multisigParticipant;
if (request.getSenderNodeAddress().equals(trade.getMakerNodeAddress())) multisigParticipant = processModel.getMaker();
@ -94,23 +84,23 @@ public class ProcessInitMultisigRequest extends TradeTask {
if (multisigParticipant.getPreparedMultisigHex() == null) multisigParticipant.setPreparedMultisigHex(request.getPreparedMultisigHex());
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 (multisigParticipant.getMadeMultisigHex() == null) multisigParticipant.setMadeMultisigHex(request.getMadeMultisigHex());
else if (!multisigParticipant.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages");
else if (!multisigParticipant.getMadeMultisigHex().equals(request.getMadeMultisigHex())) throw new RuntimeException("Message's made multisig differs from previous messages: " + request.getMadeMultisigHex() + " versus " + multisigParticipant.getMadeMultisigHex());
// prepare multisig if applicable
boolean updateParticipants = false;
if (processModel.getPreparedMultisigHex() == null) {
System.out.println("Preparing multisig wallet!");
log.info("Preparing multisig wallet for trade {}", trade.getId());
multisigWallet = xmrWalletService.createMultisigWallet(trade.getId());
processModel.setPreparedMultisigHex(multisigWallet.prepareMultisig());
updateParticipants = true;
} else {
} else if (!processModel.isMultisigSetupComplete()) {
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!");
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;
@ -118,9 +108,11 @@ public class ProcessInitMultisigRequest extends TradeTask {
// exchange multisig keys if applicable
if (!processModel.isMultisigSetupComplete() && peers[0].getMadeMultisigHex() != null && peers[1].getMadeMultisigHex() != null) {
System.out.println("Exchanging multisig wallet!");
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);
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
@ -153,35 +145,40 @@ public class ProcessInitMultisigRequest extends TradeTask {
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() {
// send to peer 1
sendInitMultisigRequest(peer1Address, peer1PubKeyRing, new SendDirectMessageListener() {
@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();
public void onArrived() {
log.info("{} arrived: peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), peer1Address, request.getTradeId(), request.getUid());
ack1 = true;
if (ack1 && ack2) completeAux();
}
} else {
if (!failed) {
failed = true;
failed(ackMessage.getErrorMessage()); // TODO: (woodser): only fail once? build into task?
@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();
}
}
}
};
trade.addListener(ackListener);
});
// send to peers
sendInitMultisigRequest(peer1Address, peer1PubKeyRing);
sendInitMultisigRequest(peer2Address, peer2PubKeyRing);
// 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);
}
@ -202,9 +199,9 @@ public class ProcessInitMultisigRequest extends TradeTask {
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(
processModel.getOffer().getId(),
processModel.getMyNodeAddress(),
@ -216,22 +213,10 @@ public class ProcessInitMultisigRequest extends TradeTask {
processModel.getMadeMultisigHex());
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() {
@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();
}
});
processModel.getP2PService().sendEncryptedDirectMessage(recipient, pubKeyRing, request, listener);
}
private void completeAux() {
multisigWallet.save();
complete();
}
}

View File

@ -61,12 +61,6 @@ public class ProcessPaymentAccountPayloadRequest extends TradeTask {
// set payment account payload
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
processModel.getTradeManager().requestPersistence();
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() {
if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe();
}

View File

@ -26,12 +26,11 @@ import bisq.common.util.Utilities;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.TradeListener;
import bisq.core.trade.protocol.TradingPeer;
import bisq.network.p2p.AckMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.Date;
@ -43,7 +42,6 @@ public class ProcessSignContractRequest extends TradeTask {
private boolean ack1 = false;
private boolean ack2 = false;
private boolean failed = false;
@SuppressWarnings({"unused"})
public ProcessSignContractRequest(TaskRunner taskHandler, Trade trade) {
@ -82,44 +80,6 @@ public class ProcessSignContractRequest extends TradeTask {
trade.setContractAsJson(contractAsJson);
trade.getSelf().setContractSignature(signature);
// get response recipients. only arbitrator sends response to both peers
NodeAddress recipient1 = trade instanceof ArbitratorTrade ? trade.getMakerNodeAddress() : trade.getTradingPeerNodeAddress();
PubKeyRing recipient1PubKey = trade instanceof ArbitratorTrade ? trade.getMakerPubKeyRing() : trade.getTradingPeerPubKeyRing();
NodeAddress recipient2 = trade instanceof ArbitratorTrade ? trade.getTakerNodeAddress() : null;
PubKeyRing recipient2PubKey = trade instanceof ArbitratorTrade ? trade.getTakerPubKeyRing() : null;
// complete on successful ack messages
TradeListener ackListener = new TradeListener() {
@Override
public void onAckMessage(AckMessage ackMessage, NodeAddress sender) {
if (!ackMessage.getSourceMsgClassName().equals(SignContractResponse.class.getSimpleName())) return;
if (ackMessage.isSuccess()) {
if (sender.equals(trade.getTradingPeerNodeAddress())) ack1 = true;
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?
}
}
}
};
trade.addListener(ackListener);
// send contract signature response(s)
if (recipient1 != null) sendSignContractResponse(recipient1, recipient1PubKey, signature);
if (recipient2 != null) sendSignContractResponse(recipient2, recipient2PubKey, signature);
} catch (Throwable t) {
failed(t);
}
}
private void sendSignContractResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, String contractSignature) {
// create response with contract signature
SignContractResponse response = new SignContractResponse(
trade.getOffer().getId(),
@ -128,20 +88,52 @@ public class ProcessSignContractRequest extends TradeTask {
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
contractSignature);
signature);
// send request
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() {
// get response recipients. only arbitrator sends response to both peers
NodeAddress recipient1 = trade instanceof ArbitratorTrade ? trade.getMakerNodeAddress() : trade.getTradingPeerNodeAddress();
PubKeyRing recipient1PubKey = trade instanceof ArbitratorTrade ? trade.getMakerPubKeyRing() : trade.getTradingPeerPubKeyRing();
NodeAddress recipient2 = trade instanceof ArbitratorTrade ? trade.getTakerNodeAddress() : null;
PubKeyRing recipient2PubKey = trade instanceof ArbitratorTrade ? trade.getTakerPubKeyRing() : null;
// send response to recipient 1
processModel.getP2PService().sendEncryptedDirectMessage(recipient1, recipient1PubKey, response, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), nodeAddress, trade.getId());
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), recipient1, trade.getId());
ack1 = true;
if (ack1 && (recipient2 == null || ack2)) complete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), nodeAddress, trade.getId(), 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 response to recipient 2 if applicable
if (recipient2 != null) {
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) {
failed(t);
}
}
}

View File

@ -27,7 +27,6 @@ import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.TradingPeer;
import bisq.network.p2p.SendDirectMessageListener;
import common.utils.GenUtils;
import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@ -45,12 +44,6 @@ public class ProcessSignContractResponse extends TradeTask {
try {
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
String contractAsJson = trade.getContractAsJson();
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) {
// start listening for deposit txs
trade.setupDepositTxsListener();
trade.listenForDepositTxs();
// create request for arbitrator to deposit funds to multisig
DepositRequest request = new DepositRequest(
@ -95,6 +88,8 @@ public class ProcessSignContractResponse extends TradeTask {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId());
processModel.getTradeManager().requestPersistence();
complete();
}
@Override
public void onFault(String errorMessage) {
@ -103,11 +98,9 @@ public class ProcessSignContractResponse extends TradeTask {
failed();
}
});
} else {
complete(); // does not yet have needed signatures
}
// persist and complete
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);
}

View File

@ -75,6 +75,9 @@ public class ProcessUpdateMultisigRequest extends TradeTask {
int numOutputsSigned = multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
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
UpdateMultisigResponse response = new UpdateMultisigResponse(
processModel.getOffer().getId(),
@ -90,9 +93,6 @@ public class ProcessUpdateMultisigRequest extends TradeTask {
@Override
public void onArrived() {
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();
}
@Override

View File

@ -44,9 +44,8 @@ import monero.wallet.model.MoneroTxWallet;
@Slf4j
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 failed = false;
@SuppressWarnings({"unused"})
public SendSignContractRequestAfterMultisig(TaskRunner taskHandler, Trade trade) {
@ -58,11 +57,19 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
try {
runInterceptHook();
synchronized (trade.getXmrWalletService().getWallet()) { // synchronize on wallet to create deposit tx and freeze funds
// skip if multisig wallet not complete
if (!processModel.isMultisigSetupComplete()) return; // TODO: woodser: this does not ack original request?
if (!processModel.isMultisigSetupComplete()) {
complete();
return; // TODO: woodser: this does not ack original request?
}
// skip if deposit tx already created
if (processModel.getDepositTxXmr() != null) return;
if (processModel.getDepositTxXmr() != null) {
complete();
return;
}
// thaw reserved outputs
MoneroWallet wallet = trade.getXmrWalletService().getWallet();
@ -74,8 +81,7 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
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();
String multisigAddress = processModel.getMultisigAddress();
MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount);
// freeze deposit outputs
@ -88,44 +94,12 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
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
sendSignContractRequest(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), offer, depositTx);
sendSignContractRequest(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), offer, depositTx);
} catch (Throwable t) {
failed(t);
}
}
private void sendSignContractRequest(NodeAddress nodeAddress, PubKeyRing pubKeyRing, Offer offer, MoneroTxWallet depositTx) {
// create request to sign contract
// create request for peer and arbitrator 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
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
trade.getProcessModel().getAccountId(),
@ -133,19 +107,41 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
depositTx.getHash());
// send request
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, request, new SendDirectMessageListener() {
// 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(), nodeAddress, trade.getId());
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(), nodeAddress, trade.getId(), 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) {
failed(t);
}
}
private void completeAux() {

View File

@ -33,7 +33,7 @@ public class SetupDepositTxsListener extends TradeTask {
protected void run() {
try {
runInterceptHook();
trade.setupDepositTxsListener();
trade.listenForDepositTxs();
complete();
} catch (Throwable t) {
failed(t);

View File

@ -57,7 +57,7 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
// fetch relevant trade info
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
if (!multisigWallet.isMultisigImportNeeded()) {
@ -74,7 +74,6 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
UpdateMultisigResponse response = (UpdateMultisigResponse) message;
multisigWallet.importMultisigHex(Arrays.asList(response.getUpdatedMultisigHex()));
multisigWallet.sync();
multisigWallet.save();
trade.removeListener(updateMultisigResponseListener);
complete();
}

View File

@ -18,7 +18,6 @@
package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.offer.Offer;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.Trade;
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.wallet.MoneroWallet;
import monero.wallet.model.MoneroAccount;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroSubaddress;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
@ -65,7 +62,7 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask {
Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() 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
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!!!");
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))) // reduce payment amount to compute fee of similar tx
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5)))
.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(9)).divide(BigInteger.valueOf(10)))
.setRelay(false)
);
@ -98,19 +95,18 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask {
numAttempts++;
payoutTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(new MoneroDestination(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(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount
.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))
.setRelay(false));
} catch (MoneroError e) {
//e.printStackTrace();
//System.out.println("FAILED TO CREATE PAYOUT TX, ITERATING...");
// exception expected
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx");
System.out.println("PAYOUT TX GENERATED ON ATTEMPT " + numAttempts);
System.out.println(payoutTx);
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
processModel.setBuyerSignedPayoutTx(payoutTx);
walletService.closeMultisigWallet(trade.getId());
complete();
} catch (Throwable t) {
failed(t);

View File

@ -63,6 +63,7 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask {
trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0)));
XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx());
trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG);
walletService.closeMultisigWallet(trade.getId());
//processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId());
} else {
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId());

View File

@ -18,7 +18,6 @@
package bisq.core.trade.protocol.tasks.seller;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.offer.Offer;
import bisq.core.trade.Contract;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.Trade;
@ -31,10 +30,6 @@ import java.math.BigInteger;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
@ -59,29 +54,17 @@ public class SellerSignAndPublishPayoutTx extends TradeTask {
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
String buyerSignedPayoutTxHex = trade.getTradingPeer().getSignedPayoutTxHex();
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 buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount();
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
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): nack
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): test nack
MoneroTxWallet buyerSignedPayoutTx = parsedTxSet.getTxs().get(0);
System.out.println("Parsed buyer signed tx hex:\n" + buyerSignedPayoutTx);
// 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");
// 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 (!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
if (!buyerSignedPayoutTx.getChangeAmount().equals(new BigInteger("0")) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify change address is multisig'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
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
multisigWallet.submitMultisigTxHex(signedMultisigTxHex);
// update state
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
if (parsedTxSet.getTxs().get(0).getTxSet() != parsedTxSet) System.out.println("LINKS ARE WRONG!!!");
// close multisig wallet
walletService.closeMultisigWallet(trade.getId());
// update trade state
parsedTxSet.setMultisigTxHex(signedMultisigTxHex); // TODO (woodser): better place to store this?
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");
//
// Offer offer = trade.getOffer();
// TradingPeer tradingPeer = trade.getTradingPeer();
// BtcWalletService walletService = processModel.getBtcWalletService();
// String id = processModel.getOffer().getId();
//
// 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) {
failed(t);
}

View File

@ -41,28 +41,27 @@ public class TakerReservesTradeFunds extends TradeTask {
try {
runInterceptHook();
// synchronize on wallet to reserve key images
synchronized (model.getXmrWalletService().getWallet()) {
// create transaction to reserve trade
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong());
MoneroTxWallet reserveTx = TradeUtils.createReserveTx(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount);
MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount);
// freeze trade funds
// TODO (woodser): synchronize to handle potential race condition where concurrent trades freeze each other's outputs
List<String> reserveTxKeyImages = new ArrayList<String>();
MoneroWallet wallet = model.getXmrWalletService().getWallet();
for (MoneroOutput input : reserveTx.getInputs()) {
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(reserveTxKeyImages);
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();
}
} catch (Throwable t) {
trade.setErrorMessage("An error occurred.\n" +
"Error message:\n"

View File

@ -42,12 +42,11 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
try {
runInterceptHook();
// send request to offer signer
// send request to arbitrator
sendInitTradeRequest(trade.getOffer().getOfferPayload().getArbitratorSigner(), new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
complete();
}
// send request to backup arbitrator if signer unavailable
@ -63,7 +62,6 @@ public class TakerSendsInitTradeRequestToArbitrator extends TradeTask {
@Override
public void onArrived() {
log.info("{} arrived at backup arbitrator: offerId={}", InitTradeRequest.class.getSimpleName(), trade.getId());
complete();
}
@Override
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) {
failed(t);
}

View File

@ -70,7 +70,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
}
@Override
public void handleErrorMessage(String errorMessage) {
public synchronized void handleErrorMessage(String errorMessage) {
// A task runner may call handleErrorMessage(String) more than once.
// Throw only one exception if that happens, to avoid looping until the
// grpc stream is closed

View File

@ -47,7 +47,7 @@ class GrpcExceptionHandler {
public GrpcExceptionHandler() {
}
public void handleException(Logger log,
public synchronized void handleException(Logger log,
Throwable t,
StreamObserver<?> responseObserver) {
// 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;
}
public void handleExceptionAsWarning(Logger log,
public synchronized void handleExceptionAsWarning(Logger log,
String calledMethod,
Throwable t,
StreamObserver<?> responseObserver) {

View File

@ -8,7 +8,7 @@ import bisq.proto.grpc.NotificationsGrpc.NotificationsImplBase;
import bisq.proto.grpc.RegisterNotificationListenerRequest;
import bisq.proto.grpc.SendNotificationReply;
import bisq.proto.grpc.SendNotificationRequest;
import io.grpc.Context;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
@ -46,17 +46,22 @@ class GrpcNotificationsService extends NotificationsImplBase {
@Override
public void registerNotificationListener(RegisterNotificationListenerRequest request,
StreamObserver<NotificationMessage> responseObserver) {
Context ctx = Context.current().fork(); // context is independent for long-lived request
ctx.run(() -> {
try {
coreApi.addNotificationListener(new GrpcNotificationListener(responseObserver));
// No onNext / onCompleted, as the response observer should be kept open
} catch (Throwable t) {
exceptionHandler.handleException(log, t, responseObserver);
}
});
}
@Override
public void sendNotification(SendNotificationRequest request,
StreamObserver<SendNotificationReply> responseObserver) {
Context ctx = Context.current().fork(); // context is independent from notification delivery
ctx.run(() -> {
try {
coreApi.sendNotification(request.getNotification());
responseObserver.onNext(SendNotificationReply.newBuilder().build());
@ -64,6 +69,7 @@ class GrpcNotificationsService extends NotificationsImplBase {
} catch (Throwable t) {
exceptionHandler.handleException(log, t, responseObserver);
}
});
}
@Value

View File

@ -141,6 +141,11 @@ class GrpcOffersService extends OffersImplBase {
@Override
public void createOffer(CreateOfferRequest req,
StreamObserver<CreateOfferReply> responseObserver) {
GrpcErrorMessageHandler errorMessageHandler =
new GrpcErrorMessageHandler(getCreateOfferMethod().getFullMethodName(),
responseObserver,
exceptionHandler,
log);
try {
coreApi.createAnPlaceOffer(
req.getCurrencyCode(),
@ -162,6 +167,10 @@ class GrpcOffersService extends OffersImplBase {
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
},
errorMessage -> {
if (!errorMessageHandler.isErrorHandled())
errorMessageHandler.handleErrorMessage(errorMessage);
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);

View File

@ -65,7 +65,6 @@ public class GrpcServer {
GrpcMoneroConnectionsService moneroConnectionsService,
GrpcMoneroNodeService moneroNodeService) {
this.server = ServerBuilder.forPort(config.apiPort)
.executor(UserThread.getExecutor())
.addService(interceptForward(accountService, accountService.interceptors()))
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
.addService(interceptForward(disputesService, disputesService.interceptors()))

View File

@ -19,7 +19,6 @@ package bisq.daemon.grpc;
import bisq.core.api.CoreApi;
import bisq.core.api.model.TradeInfo;
import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Trade;
import bisq.proto.grpc.ConfirmPaymentReceivedReply;
@ -40,7 +39,6 @@ import bisq.proto.grpc.TakeOfferReply;
import bisq.proto.grpc.TakeOfferRequest;
import bisq.proto.grpc.WithdrawFundsReply;
import bisq.proto.grpc.WithdrawFundsRequest;
import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver;
@ -122,6 +120,7 @@ class GrpcTradesService extends TradesImplBase {
responseObserver,
exceptionHandler,
log);
try {
coreApi.takeOffer(req.getOfferId(),
req.getPaymentAccountId(),
trade -> {
@ -136,6 +135,9 @@ class GrpcTradesService extends TradesImplBase {
if (!errorMessageHandler.isErrorHandled())
errorMessageHandler.handleErrorMessage(errorMessage);
});
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
}
@Override

View File

@ -17,6 +17,7 @@
package bisq.daemon.grpc;
import bisq.common.UserThread;
import bisq.core.api.CoreApi;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.TxFeeRateInfo;
@ -102,6 +103,7 @@ class GrpcWalletsService extends WalletsImplBase {
@Override
public void getBalances(GetBalancesRequest req, StreamObserver<GetBalancesReply> responseObserver) {
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()?
try {
var balances = coreApi.getBalances(req.getCurrencyCode());
var reply = GetBalancesReply.newBuilder()
@ -112,6 +114,7 @@ class GrpcWalletsService extends WalletsImplBase {
} catch (Throwable cause) {
exceptionHandler.handleException(log, cause, responseObserver);
}
});
}
@Override

View File

@ -40,6 +40,7 @@ public class GrpcCallRateMeter {
}
public boolean checkAndIncrement() {
synchronized (callTimestamps) {
if (getCallsCount() < allowedCallsPerTimeWindow) {
incrementCallsCount();
return true;
@ -47,13 +48,17 @@ public class GrpcCallRateMeter {
return false;
}
}
}
public int getCallsCount() {
synchronized (callTimestamps) {
removeStaleCallTimestamps();
return callTimestamps.size();
}
}
public String getCallsCountProgress(String calledMethodName) {
synchronized (callTimestamps) {
String shortTimeUnitName = StringUtils.chop(timeUnit.name().toLowerCase());
// Just print 'GetVersion has been called N times...',
// not 'io.bisq.protobuffer.GetVersion/GetVersion has been called N times...'
@ -66,6 +71,7 @@ public class GrpcCallRateMeter {
allowedCallsPerTimeWindow,
shortTimeUnitName);
}
}
private void incrementCallsCount() {
callTimestamps.add(currentTimeMillis());
@ -85,6 +91,7 @@ public class GrpcCallRateMeter {
@Override
public String toString() {
synchronized (callTimestamps) {
return "GrpcCallRateMeter{" +
"allowedCallsPerTimeWindow=" + allowedCallsPerTimeWindow +
", timeUnit=" + timeUnit.name() +
@ -93,3 +100,4 @@ public class GrpcCallRateMeter {
'}';
}
}
}

View File

@ -736,9 +736,11 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
});
model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> {
UserThread.execute(() -> {
p2PNetworkIcon.setOpacity(1);
p2pNetworkProgressBar.setProgress(0);
});
});
p2pNetworkProgressBar = new JFXProgressBar(-1);
p2pNetworkProgressBar.setMaxHeight(2);

View File

@ -240,7 +240,7 @@ public class LockedView extends ActivatableView<VBox, Void> {
private Optional<Tradable> getTradable(LockedListItem item) {
String offerId = item.getAddressEntry().getOfferId();
Optional<Trade> tradeOptional = tradeManager.getTradeById(offerId);
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(offerId);
if (tradeOptional.isPresent()) {
return Optional.of(tradeOptional.get());
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {

View File

@ -239,7 +239,7 @@ public class ReservedView extends ActivatableView<VBox, Void> {
private Optional<Tradable> getTradable(ReservedListItem item) {
String offerId = item.getAddressEntry().getOfferId();
Optional<Trade> tradeOptional = tradeManager.getTradeById(offerId);
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(offerId);
if (tradeOptional.isPresent()) {
return Optional.of(tradeOptional.get());
} else if (openOfferManager.getOpenOfferById(offerId).isPresent()) {

View File

@ -32,9 +32,7 @@ import javafx.collections.ObservableList;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroTxWallet;
@ -82,14 +80,14 @@ class TransactionAwareTrade implements TransactionAwareTradable {
private boolean isMakerDepositTx(String txId) {
return Optional.ofNullable(trade.getMakerDepositTx())
.map(MoneroTxWallet::getHash)
.map(MoneroTx::getHash)
.map(hash -> hash.equals(txId))
.orElse(false);
}
private boolean isTakerDepositTx(String txId) {
return Optional.ofNullable(trade.getTakerDepositTx())
.map(MoneroTxWallet::getHash)
.map(MoneroTx::getHash)
.map(hash -> hash.equals(txId))
.orElse(false);
}

View File

@ -382,6 +382,7 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
}
private void updateChartData() {
UserThread.execute(() -> {
seriesBuy.getData().clear();
seriesSell.getData().clear();
areaChart.getData().clear();
@ -393,6 +394,7 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
seriesSell.getData().addAll(filterOutliersSell(model.getSellData(), isCrypto));
areaChart.getData().addAll(List.of(seriesBuy, seriesSell));
});
}
List<XYChart.Data<Number, Number>> filterOutliersBuy(List<XYChart.Data<Number, Number>> buy, boolean isCrypto) {

View File

@ -62,7 +62,7 @@ import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.NodeAddress;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.config.Config;
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));
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:
// https://github.com/bisq-network/bisq-desktop/issues/588

View File

@ -51,7 +51,7 @@ import bisq.network.p2p.P2PService;
import bisq.network.p2p.network.CloseConnectionReason;
import bisq.network.p2p.network.Connection;
import bisq.network.p2p.network.ConnectionListener;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import org.bitcoinj.core.Coin;
@ -524,6 +524,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
private void updateSpinnerInfo() {
UserThread.execute(() -> {
if (!showPayFundsScreenDisplayed.get() ||
offerWarning.get() != null ||
errorMessage.get() != null ||
@ -541,6 +542,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
isWaitingForFunds.set(!spinnerInfoText.get().isEmpty());
});
}
private void addListeners() {

View File

@ -515,6 +515,7 @@ public abstract class Overlay<T extends Overlay<T>> {
if (owner != null) {
Scene rootScene = owner.getScene();
if (rootScene != null) {
UserThread.execute(() -> {
Scene scene = new Scene(getRootContainer());
scene.getStylesheets().setAll(rootScene.getStylesheets());
scene.setFill(Color.TRANSPARENT);
@ -556,6 +557,7 @@ public abstract class Overlay<T extends Overlay<T>> {
animateDisplay();
isDisplayed = true;
});
}
}
}

View File

@ -64,7 +64,9 @@ public class Notification extends Overlay<Notification> {
if (autoClose && autoCloseTimer == null)
autoCloseTimer = UserThread.runAfter(this::doClose, 6);
UserThread.execute(() -> {
stage.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> doClose());
});
}
@Override

View File

@ -54,7 +54,7 @@ import bisq.core.trade.protocol.SellerProtocol;
import bisq.core.user.Preferences;
import bisq.network.p2p.P2PService;
import bisq.common.UserThread;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.PubKeyRingProvider;
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.checkNotNull;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@ -360,6 +359,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
}
private void doSelectItem(@Nullable PendingTradesListItem item) {
UserThread.execute(() -> {
if (selectedTrade != null)
selectedTrade.stateProperty().removeListener(tradeStateChangeListener);
@ -370,8 +370,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
return;
}
MoneroTxWallet makerDepositTx = selectedTrade.getMakerDepositTx();
MoneroTxWallet takerDepositTx = selectedTrade.getTakerDepositTx();
MoneroTx makerDepositTx = selectedTrade.getMakerDepositTx();
MoneroTx 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
@ -408,6 +408,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
notificationCenter.setSelectedTradeId(null);
}
selectedItemProperty.set(item);
});
}
private void tryOpenDispute(boolean isSupportTicket) {
@ -460,6 +461,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
MoneroTxWallet payoutTx = trade.getPayoutTx();
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.getMultisigHex();
xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet
if (payoutTx != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString();

View File

@ -363,7 +363,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
}
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) {

View File

@ -137,6 +137,7 @@ public class BuyerStep2View extends TradeStepView {
showPopup();
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG.ordinal()) {
if (!trade.hasFailed()) {
UserThread.execute(() -> {
switch (state) {
case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED:
case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG:
@ -170,6 +171,7 @@ public class BuyerStep2View extends TradeStepView {
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
break;
}
});
} else {
log.warn("Trade contains error message {}", trade.getErrorMessage());
statusLabel.setText("");

View File

@ -471,6 +471,7 @@ public class ChatView extends AnchorPane {
}
private void updateMsgState(ChatMessage message) {
UserThread.execute(() -> {
boolean visible;
AwesomeIcon icon = null;
String text = null;
@ -508,6 +509,7 @@ public class ChatView extends AnchorPane {
statusIcon.setTooltip(new Tooltip(text));
statusInfoLabel.setText(text);
}
});
}
};
}

View File

@ -85,6 +85,7 @@ public class DisputeChatPopup {
}
public void openChat(Dispute selectedDispute, DisputeSession concreteDisputeSession, String counterpartyName) {
UserThread.execute(() -> {
closeChat();
this.selectedDispute = selectedDispute;
selectedDispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
@ -156,5 +157,6 @@ public class DisputeChatPopup {
// 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));
});
}
}

View File

@ -1131,7 +1131,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
super.updateItem(item, empty);
if (item != null && !empty) {
Optional<Trade> tradeOptional = tradeManager.getTradeById(item.getTradeId());
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(item.getTradeId());
if (tradeOptional.isPresent()) {
field = new HyperlinkWithIcon(item.getShortTradeId());
field.setMouseTransparent(false);
@ -1349,6 +1349,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
@Override
public void updateItem(final Dispute item, boolean empty) {
super.updateItem(item, empty);
UserThread.execute(() -> {
if (item != null && !empty) {
if (closedProperty != null) {
closedProperty.removeListener(listener);
@ -1374,6 +1375,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
}
setText("");
}
});
}
};
}
@ -1389,6 +1391,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
}
private void updateChatMessageCount(Dispute dispute, JFXBadge chatBadge) {
UserThread.execute(() -> {
if (chatBadge == null)
return;
// when the chat popup is active, we do not display new message count indicator for that item
@ -1410,6 +1413,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
}
chatBadge.refreshBadge();
dispute.refreshAlertLevel(senderFlag());
});
}
private String getCounterpartyName() {

View File

@ -92,12 +92,12 @@ public class Transitions {
public void fadeOutAndRemove(Node node, int duration, EventHandler<ActionEvent> handler) {
FadeTransition fade = fadeOut(node, getDuration(duration));
fade.setInterpolator(Interpolator.EASE_IN);
fade.setOnFinished(actionEvent -> {
fade.setOnFinished(actionEvent -> UserThread.execute(() -> {
((Pane) (node.getParent())).getChildren().remove(node);
//Profiler.printMsgWithTime("fadeOutAndRemove");
if (handler != null)
handler.handle(actionEvent);
});
}));
}
// Blur

View File

@ -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.
//TODO decrease limits again after testing
private static final int SOCKET_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(180);
private static final int MAX_CONNECTION_THREADS = 10;
public static int getPermittedMessageSize() {
return PERMITTED_MESSAGE_SIZE;
@ -130,6 +131,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
@Getter
private final String uid;
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
@Getter
private final Statistic statistic;
@ -429,13 +432,19 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
// Only receive non - CloseConnectionMessage network_messages
@Override
public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) {
checkArgument(connection.equals(this));
Connection that = this;
connectionThreadPool.submit(new Runnable() {
@Override
public void run() {
checkArgument(connection.equals(that));
if (networkEnvelope instanceof BundleOfEnvelopes) {
onBundleOfEnvelopes((BundleOfEnvelopes) networkEnvelope, connection);
} else {
UserThread.execute(() -> messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection)));
messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection));
}
}
});
}
private void onBundleOfEnvelopes(BundleOfEnvelopes bundleOfEnvelopes, Connection connection) {
Map<P2PDataStorage.ByteArray, Set<NetworkEnvelope>> itemsByHash = new HashMap<>();
@ -466,8 +475,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
envelopesToProcess.add(networkEnvelope);
}
}
envelopesToProcess.forEach(envelope -> UserThread.execute(() ->
messageListeners.forEach(listener -> listener.onMessage(envelope, connection))));
envelopesToProcess.forEach(envelope ->
messageListeners.forEach(listener -> listener.onMessage(envelope, connection)));
}
@ -793,11 +802,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
return;
}
// Throttle inbound network_messages
// Throttle inbound network messages
long now = System.currentTimeMillis();
long elapsed = now - lastReadTimeStamp;
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={}",
lastReadTimeStamp, now, elapsed);
Thread.sleep(20);

View File

@ -1497,36 +1497,38 @@ message Trade {
enum State {
PB_ERROR_STATE = 0;
PREPARATION = 1;
TAKER_PUBLISHED_TAKER_FEE_TX = 2;
MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 3;
MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 4;
MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 5;
MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 6;
TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 7;
TAKER_PUBLISHED_DEPOSIT_TX = 8;
TAKER_SAW_DEPOSIT_TX_IN_NETWORK = 9;
TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 10;
TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 11;
TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 12;
TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 13;
MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 14;
MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 15;
DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 16;
BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 17;
BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 18;
BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 19;
BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 20;
BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 21;
SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 22;
SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 23;
SELLER_PUBLISHED_PAYOUT_TX = 24;
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 25;
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 26;
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 27;
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 28;
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 29;
BUYER_SAW_PAYOUT_TX_IN_NETWORK = 30;
WITHDRAW_COMPLETED = 31;
CONTRACT_SIGNATURE_REQUESTED = 2;
CONTRACT_SIGNED = 3;
TAKER_PUBLISHED_TAKER_FEE_TX = 4;
MAKER_SENT_PUBLISH_DEPOSIT_TX_REQUEST = 5;
MAKER_SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 6;
MAKER_STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 7;
MAKER_SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 8;
TAKER_RECEIVED_PUBLISH_DEPOSIT_TX_REQUEST = 9;
TAKER_PUBLISHED_DEPOSIT_TX = 10;
TAKER_SAW_DEPOSIT_TX_IN_NETWORK = 11;
TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG = 12;
TAKER_SAW_ARRIVED_DEPOSIT_TX_PUBLISHED_MSG = 13;
TAKER_STORED_IN_MAILBOX_DEPOSIT_TX_PUBLISHED_MSG = 14;
TAKER_SEND_FAILED_DEPOSIT_TX_PUBLISHED_MSG = 15;
MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 16;
MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 17;
DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 18;
BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 19;
BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 20;
BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 21;
BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 22;
BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 23;
SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 24;
SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 25;
SELLER_PUBLISHED_PAYOUT_TX = 26;
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 27;
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 28;
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 29;
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 30;
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 31;
BUYER_SAW_PAYOUT_TX_IN_NETWORK = 32;
WITHDRAW_COMPLETED = 33;
}
enum Phase {
@ -1654,9 +1656,8 @@ message ProcessModel {
NodeAddress temp_trading_peer_node_address = 1006;
string prepared_multisig_hex = 1007;
string made_multisig_hex = 1008;
bool multisig_setup_complete = 1009;
bool maker_ready_to_fund_multisig = 1010;
bool multisig_deposit_initiated = 1011;
string multisig_address = 1009;
bool multisig_setup_complete = 1010; // TODO: remove this field
}
message TradingPeer {