refactor payout protocol

send payment key & multisig hex on deposit confirm for resilience
support payout published, confirmed, unlocked states
keep trade wallets open throughout trade
close and delete trade wallets when payout unlocks
arbitrator idles trade wallets after deposits confirm (1/hour)
This commit is contained in:
woodser 2022-10-26 01:05:09 -04:00
parent 45bac8c264
commit f36dde2857
84 changed files with 1486 additions and 2272 deletions

View file

@ -16,7 +16,7 @@ import org.junit.jupiter.api.TestInfo;
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL; import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
import static bisq.core.trade.Trade.Phase.DEPOSITS_UNLOCKED; import static bisq.core.trade.Trade.Phase.DEPOSITS_UNLOCKED;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT; import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG; import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG;
import static bisq.core.trade.Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; import static bisq.core.trade.Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN;
import static bisq.core.trade.Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG; import static bisq.core.trade.Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG;
@ -150,7 +150,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
String tradeId) { String tradeId) {
Predicate<TradeInfo> isTradeInPaymentReceiptConfirmedStateAndPhase = (t) -> Predicate<TradeInfo> isTradeInPaymentReceiptConfirmedStateAndPhase = (t) ->
t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name()) && t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name()) &&
(t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name())); t.getPhase().equals(PAYMENT_SENT.name());
String userName = toUserName.apply(grpcClient); String userName = toUserName.apply(grpcClient);
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) { for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
TradeInfo trade = grpcClient.getTrade(tradeId); TradeInfo trade = grpcClient.getTrade(tradeId);

View file

@ -30,13 +30,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.USD; import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.BUY;
import static protobuf.OpenOffer.State.AVAILABLE; import static protobuf.OpenOffer.State.AVAILABLE;
@ -113,8 +110,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
trade = bobClient.getTrade(tradeId); trade = bobClient.getTrade(tradeId);
// Note: offer.state == available // Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState()); assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setPaymentReceivedMessageSent(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);

View file

@ -50,8 +50,8 @@ import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.Offer.State.OFFER_FEE_RESERVED;
import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.BUY;
@ -200,8 +200,8 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
trade = bobClient.getTrade(tradeId); trade = bobClient.getTrade(tradeId);
// Note: offer.state == available // Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState()); assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setPaymentReceivedMessageSent(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);

View file

@ -29,11 +29,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.Offer.State.OFFER_FEE_RESERVED; import static protobuf.Offer.State.OFFER_FEE_RESERVED;
@ -130,8 +129,8 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
trade = aliceClient.getTrade(tradeId); trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setPaymentReceivedMessageSent(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);

View file

@ -32,10 +32,10 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.USD; import static bisq.apitest.config.ApiTestConfig.USD;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static bisq.core.trade.Trade.Phase.WITHDRAWN; import static bisq.core.trade.Trade.Phase.COMPLETED;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED; import static bisq.core.trade.Trade.State.TRADE_COMPLETED;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -119,8 +119,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
sleep(3_000); sleep(3_000);
trade = aliceClient.getTrade(tradeId); trade = aliceClient.getTrade(tradeId);
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState()); assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setPaymentReceivedMessageSent(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);

View file

@ -32,12 +32,9 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BTC; import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.apitest.config.ApiTestConfig.XMR; import static bisq.apitest.config.ApiTestConfig.XMR;
import static bisq.cli.table.builder.TableType.OFFER_TBL; import static bisq.cli.table.builder.TableType.OFFER_TBL;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED; import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
import static bisq.core.trade.Trade.Phase.WITHDRAWN; import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
import static protobuf.OfferDirection.BUY; import static protobuf.OfferDirection.BUY;
@ -139,8 +136,8 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
trade = bobClient.getTrade(tradeId); trade = bobClient.getTrade(tradeId);
// Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED. // Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED.
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG) EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
.setPhase(PAYOUT_PUBLISHED) .setPhase(PAYMENT_RECEIVED)
.setPayoutPublished(true) .setPayoutPublished(true)
.setPaymentReceivedMessageSent(true); .setPaymentReceivedMessageSent(true);
verifyExpectedProtocolStatus(trade); verifyExpectedProtocolStatus(trade);

View file

@ -105,9 +105,6 @@ public class CoreDisputesService {
String updatedMultisigHex = multisigWallet.exportMultisigHex(); String updatedMultisigHex = multisigWallet.exportMultisigHex();
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler); disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
tradeManager.requestPersistence(); tradeManager.requestPersistence();
// close multisig wallet
xmrWalletService.closeMultisigWallet(trade.getId());
} }
} }
@ -159,7 +156,8 @@ public class CoreDisputesService {
if (disputeOptional.isPresent()) dispute = disputeOptional.get(); if (disputeOptional.isPresent()) dispute = disputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (tradeManager.getTrade(tradeId)) { Trade trade = tradeManager.getTrade(tradeId);
synchronized (trade) {
var closeDate = new Date(); var closeDate = new Date();
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
var contract = dispute.getContract(); var contract = dispute.getContract();
@ -176,8 +174,8 @@ public class CoreDisputesService {
} }
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
// resolve the payout // apply dispute payout
resolveDisputePayout(dispute, disputeResult, contract); applyDisputePayout(dispute, disputeResult, contract);
// close dispute ticket // close dispute ticket
closeDispute(arbitrationManager, dispute, disputeResult, false); closeDispute(arbitrationManager, dispute, disputeResult, false);
@ -186,19 +184,17 @@ public class CoreDisputesService {
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
.findFirst(); .findFirst();
if (peersDisputeOptional.isPresent()) { if (peersDisputeOptional.isPresent()) {
var peerDispute = peersDisputeOptional.get(); var peerDispute = peersDisputeOptional.get();
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher()); peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
} else { } else {
throw new IllegalStateException("could not find peer dispute"); throw new IllegalStateException("could not find peer dispute");
} }
arbitrationManager.requestPersistence(); arbitrationManager.requestPersistence();
} }
} catch (Exception e) { } catch (Exception e) {
@ -250,7 +246,7 @@ public class CoreDisputesService {
} }
} }
public void resolveDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) { public void applyDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) {
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master) // TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
if (!dispute.isMediationDispute()) { if (!dispute.isMediationDispute()) {
try { try {
@ -259,30 +255,25 @@ public class CoreDisputesService {
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
// TODO (woodser): don't send signed tx if opener is not co-signer? // determine if dispute is in context of publisher
// // determine if opener is co-signer boolean isOpener = dispute.isOpener();
// boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER); boolean isWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.SELLER);
// boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher(); boolean isPublisher = disputeResult.isLoserPublisher() ? !isWinner : isWinner;
// if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx");
// open multisig wallet // open multisig wallet
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); 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 // if dispute is in context of opener, arbitrator has multisig hex to create and validate payout tx
boolean isOpener = dispute.isOpener();
System.out.println("Is dispute opener: " + isOpener);
if (isOpener) { if (isOpener) {
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
if (arbitratorPayoutTx != null)
disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); // if opener is publisher, include signed payout tx in dispute result, otherwise publisher must request payout tx by providing updated multisig hex
if (isPublisher) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
} }
// send arbitrator's updated multisig hex with dispute result // send arbitrator's updated multisig hex with dispute result
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex()); disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex());
// close multisig wallet
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
} }
} catch (AddressFormatException e2) { } catch (AddressFormatException e2) {
log.error("Error at close dispute", e2); log.error("Error at close dispute", e2);

View file

@ -5,7 +5,7 @@ import bisq.common.config.Config;
import bisq.core.btc.model.EncryptedConnectionList; import bisq.core.btc.model.EncryptedConnectionList;
import bisq.core.btc.setup.DownloadListener; import bisq.core.btc.setup.DownloadListener;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
@ -239,7 +239,7 @@ public final class CoreMoneroConnectionsService {
public long getDefaultRefreshPeriodMs() { public long getDefaultRefreshPeriodMs() {
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS; if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
else { else {
boolean isLocal = TradeUtils.isLocalHost(daemon.getRpcConnection().getUri()); boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri());
if (isLocal) { if (isLocal) {
updateDaemonInfo(); updateDaemonInfo();
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped
@ -363,7 +363,7 @@ public final class CoreMoneroConnectionsService {
// if offline and last connection is local, start local node if offline // if offline and last connection is local, start local node if offline
currentConnectionUri.ifPresent(uri -> { currentConnectionUri.ifPresent(uri -> {
try { try {
if (!connectionManager.isConnected() && TradeUtils.isLocalHost(uri) && !nodeService.isOnline()) { if (!connectionManager.isConnected() && HavenoUtils.isLocalHost(uri) && !nodeService.isOnline()) {
nodeService.startMoneroNode(); nodeService.startMoneroNode();
} }
} catch (Exception e) { } catch (Exception e) {
@ -372,7 +372,7 @@ public final class CoreMoneroConnectionsService {
}); });
// prefer to connect to local node unless prevented by configuration // prefer to connect to local node unless prevented by configuration
if (("".equals(config.xmrNode) || TradeUtils.isLocalHost(config.xmrNode)) && if (("".equals(config.xmrNode) || HavenoUtils.isLocalHost(config.xmrNode)) &&
(!connectionManager.isConnected() || connectionManager.getAutoSwitch()) && (!connectionManager.isConnected() || connectionManager.getAutoSwitch()) &&
nodeService.isConnected()) { nodeService.isConnected()) {
MoneroRpcConnection connection = connectionManager.getConnectionByUri(nodeService.getDaemon().getRpcConnection().getUri()); MoneroRpcConnection connection = connectionManager.getConnectionByUri(nodeService.getDaemon().getRpcConnection().getUri());

View file

@ -16,7 +16,7 @@
*/ */
package bisq.core.api; package bisq.core.api;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.xmr.MoneroNodeSettings; import bisq.core.xmr.MoneroNodeSettings;
import bisq.common.config.BaseCurrencyNetwork; import bisq.common.config.BaseCurrencyNetwork;
@ -71,7 +71,7 @@ public class CoreMoneroNodeService {
else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081; else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081;
else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081; else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081;
else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet"); else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet");
this.daemon = new MoneroDaemonRpc("http://" + TradeUtils.LOOPBACK_HOST + ":" + rpcPort); this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort);
} }
public void addListener(MoneroNodeServiceListener listener) { public void addListener(MoneroNodeServiceListener listener) {

View file

@ -79,12 +79,13 @@ public class TradeInfo implements Payload {
private final String state; private final String state;
private final String phase; private final String phase;
private final String periodState; private final String periodState;
private final String payoutState;
private final boolean isDepositPublished; private final boolean isDepositPublished;
private final boolean isDepositUnlocked; private final boolean isDepositUnlocked;
private final boolean isPaymentSent; private final boolean isPaymentSent;
private final boolean isPaymentReceived; private final boolean isPaymentReceived;
private final boolean isPayoutPublished;
private final boolean isCompleted; private final boolean isCompleted;
private final boolean isPayoutPublished;
private final String contractAsJson; private final String contractAsJson;
private final ContractInfo contract; private final ContractInfo contract;
@ -107,6 +108,7 @@ public class TradeInfo implements Payload {
this.state = builder.getState(); this.state = builder.getState();
this.phase = builder.getPhase(); this.phase = builder.getPhase();
this.periodState = builder.getPeriodState(); this.periodState = builder.getPeriodState();
this.payoutState = builder.getPayoutState();
this.isDepositPublished = builder.isDepositPublished(); this.isDepositPublished = builder.isDepositPublished();
this.isDepositUnlocked = builder.isDepositUnlocked(); this.isDepositUnlocked = builder.isDepositUnlocked();
this.isPaymentSent = builder.isPaymentSent(); this.isPaymentSent = builder.isPaymentSent();
@ -158,6 +160,7 @@ public class TradeInfo implements Payload {
.withState(trade.getState().name()) .withState(trade.getState().name())
.withPhase(trade.getPhase().name()) .withPhase(trade.getPhase().name())
.withPeriodState(trade.getPeriodState().name()) .withPeriodState(trade.getPeriodState().name())
.withPayoutState(trade.getPayoutState().name())
.withIsDepositPublished(trade.isDepositPublished()) .withIsDepositPublished(trade.isDepositPublished())
.withIsDepositUnlocked(trade.isDepositUnlocked()) .withIsDepositUnlocked(trade.isDepositUnlocked())
.withIsPaymentSent(trade.isPaymentSent()) .withIsPaymentSent(trade.isPaymentSent())
@ -195,12 +198,13 @@ public class TradeInfo implements Payload {
.setState(state) .setState(state)
.setPhase(phase) .setPhase(phase)
.setPeriodState(periodState) .setPeriodState(periodState)
.setPayoutState(payoutState)
.setIsDepositPublished(isDepositPublished) .setIsDepositPublished(isDepositPublished)
.setIsDepositUnlocked(isDepositUnlocked) .setIsDepositUnlocked(isDepositUnlocked)
.setIsPaymentSent(isPaymentSent) .setIsPaymentSent(isPaymentSent)
.setIsPaymentReceived(isPaymentReceived) .setIsPaymentReceived(isPaymentReceived)
.setIsCompleted(isCompleted)
.setIsPayoutPublished(isPayoutPublished) .setIsPayoutPublished(isPayoutPublished)
.setIsPayoutPublished(isCompleted)
.setContractAsJson(contractAsJson == null ? "" : contractAsJson) .setContractAsJson(contractAsJson == null ? "" : contractAsJson)
.setContract(contract.toProtoMessage()) .setContract(contract.toProtoMessage())
.build(); .build();
@ -222,6 +226,7 @@ public class TradeInfo implements Payload {
.withPrice(proto.getPrice()) .withPrice(proto.getPrice())
.withVolume(proto.getTradeVolume()) .withVolume(proto.getTradeVolume())
.withPeriodState(proto.getPeriodState()) .withPeriodState(proto.getPeriodState())
.withPayoutState(proto.getPayoutState())
.withState(proto.getState()) .withState(proto.getState())
.withPhase(proto.getPhase()) .withPhase(proto.getPhase())
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress()) .withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
@ -230,8 +235,8 @@ public class TradeInfo implements Payload {
.withIsDepositUnlocked(proto.getIsDepositUnlocked()) .withIsDepositUnlocked(proto.getIsDepositUnlocked())
.withIsPaymentSent(proto.getIsPaymentSent()) .withIsPaymentSent(proto.getIsPaymentSent())
.withIsPaymentReceived(proto.getIsPaymentReceived()) .withIsPaymentReceived(proto.getIsPaymentReceived())
.withIsPayoutPublished(proto.getIsPayoutPublished())
.withIsCompleted(proto.getIsCompleted()) .withIsCompleted(proto.getIsCompleted())
.withIsPayoutPublished(proto.getIsPayoutPublished())
.withContractAsJson(proto.getContractAsJson()) .withContractAsJson(proto.getContractAsJson())
.withContract((ContractInfo.fromProto(proto.getContract()))) .withContract((ContractInfo.fromProto(proto.getContract())))
.build(); .build();
@ -256,12 +261,13 @@ public class TradeInfo implements Payload {
", state='" + state + '\'' + "\n" + ", state='" + state + '\'' + "\n" +
", phase='" + phase + '\'' + "\n" + ", phase='" + phase + '\'' + "\n" +
", periodState='" + periodState + '\'' + "\n" + ", periodState='" + periodState + '\'' + "\n" +
", payoutState='" + payoutState + '\'' + "\n" +
", isDepositPublished=" + isDepositPublished + "\n" + ", isDepositPublished=" + isDepositPublished + "\n" +
", isDepositConfirmed=" + isDepositUnlocked + "\n" + ", isDepositConfirmed=" + isDepositUnlocked + "\n" +
", isPaymentSent=" + isPaymentSent + "\n" + ", isPaymentSent=" + isPaymentSent + "\n" +
", isPaymentReceived=" + isPaymentReceived + "\n" + ", isPaymentReceived=" + isPaymentReceived + "\n" +
", isPayoutPublished=" + isPayoutPublished + "\n" +
", isCompleted=" + isCompleted + "\n" + ", isCompleted=" + isCompleted + "\n" +
", isPayoutPublished=" + isPayoutPublished + "\n" +
", offer=" + offer + "\n" + ", offer=" + offer + "\n" +
", contractAsJson=" + contractAsJson + "\n" + ", contractAsJson=" + contractAsJson + "\n" +
", contract=" + contract + "\n" + ", contract=" + contract + "\n" +

View file

@ -51,6 +51,7 @@ public final class TradeInfoV1Builder {
private String state; private String state;
private String phase; private String phase;
private String periodState; private String periodState;
private String payoutState;
private boolean isDepositPublished; private boolean isDepositPublished;
private boolean isDepositUnlocked; private boolean isDepositUnlocked;
private boolean isPaymentSent; private boolean isPaymentSent;
@ -130,12 +131,7 @@ public final class TradeInfoV1Builder {
this.volume = volume; this.volume = volume;
return this; return this;
} }
public TradeInfoV1Builder withPeriodState(String periodState) {
this.periodState = periodState;
return this;
}
public TradeInfoV1Builder withState(String state) { public TradeInfoV1Builder withState(String state) {
this.state = state; this.state = state;
return this; return this;
@ -146,6 +142,16 @@ public final class TradeInfoV1Builder {
return this; return this;
} }
public TradeInfoV1Builder withPeriodState(String periodState) {
this.periodState = periodState;
return this;
}
public TradeInfoV1Builder withPayoutState(String payoutState) {
this.payoutState = payoutState;
return this;
}
public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) { public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) {
this.arbitratorNodeAddress = arbitratorNodeAddress; this.arbitratorNodeAddress = arbitratorNodeAddress;
return this; return this;

View file

@ -27,6 +27,7 @@ import bisq.core.provider.price.PriceFeedService;
import bisq.core.setup.CorePersistedDataHost; import bisq.core.setup.CorePersistedDataHost;
import bisq.core.setup.CoreSetup; import bisq.core.setup.CoreSetup;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.trade.TradeManager;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.core.trade.txproof.xmr.XmrTxProofService;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
@ -279,6 +280,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(XmrTxProofService.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown();
injector.getInstance(TradeManager.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService
log.info("OpenOfferManager shutdown started"); log.info("OpenOfferManager shutdown started");
injector.getInstance(OpenOfferManager.class).shutDown(() -> { injector.getInstance(OpenOfferManager.class).shutDown(() -> {

View file

@ -556,7 +556,6 @@ public class HavenoSetup {
return null; return null;
} }
@Nullable
public static boolean getResyncSpvSemaphore() { public static boolean getResyncSpvSemaphore() {
File resyncSpvSemaphore = new File(Config.appDataDir(), RESYNC_SPV_FILE_NAME); File resyncSpvSemaphore = new File(Config.appDataDir(), RESYNC_SPV_FILE_NAME);
return resyncSpvSemaphore.exists(); return resyncSpvSemaphore.exists();

View file

@ -0,0 +1,13 @@
package bisq.core.btc.wallet;
import java.util.Map;
import monero.daemon.model.MoneroKeyImageSpentStatus;
public interface MoneroKeyImageListener {
/**
* Called with changes to the spent status of key images.
*/
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses);
}

View file

@ -0,0 +1,197 @@
package bisq.core.btc.wallet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import monero.common.MoneroError;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroKeyImageSpentStatus;
/**
* Poll for changes to the spent status of key images.
*/
public class MoneroKeyImagePoller {
private MoneroDaemon daemon;
private long refreshPeriodMs;
private List<String> keyImages = new ArrayList<String>();
private Set<MoneroKeyImageListener> listeners = new HashSet<MoneroKeyImageListener>();
private TaskLooper looper;
private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
/**
* Construct the listener.
*
* @param refreshPeriodMs - refresh period in milliseconds
* @param keyImages - key images to listen to
*/
public MoneroKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs, String... keyImages) {
looper = new TaskLooper(() -> poll());
setDaemon(daemon);
setRefreshPeriodMs(refreshPeriodMs);
setKeyImages(keyImages);
}
/**
* Add a listener to receive notifications.
*
* @param listener - the listener to add
*/
public void addListener(MoneroKeyImageListener listener) {
listeners.add(listener);
refreshPolling();
}
/**
* Remove a listener to receive notifications.
*
* @param listener - the listener to remove
*/
public void removeListener(MoneroKeyImageListener listener) {
if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered");
listeners.remove(listener);
refreshPolling();
}
/**
* Set the Monero daemon to fetch key images from.
*
* @param daemon - the daemon to fetch key images from
*/
public void setDaemon(MoneroDaemon daemon) {
this.daemon = daemon;
}
/**
* Get the Monero daemon to fetch key images from.
*
* @return the daemon to fetch key images from
*/
public MoneroDaemon getDaemon() {
return daemon;
}
/**
* Set the refresh period in milliseconds.
*
* @param refreshPeriodMs - the refresh period in milliseconds
*/
public void setRefreshPeriodMs(long refreshPeriodMs) {
this.refreshPeriodMs = refreshPeriodMs;
}
/**
* Get the refresh period in milliseconds
*
* @return the refresh period in milliseconds
*/
public long getRefreshPeriodMs() {
return refreshPeriodMs;
}
/**
* Get a copy of the key images being listened to.
*
* @return the key images to listen to
*/
public Collection<String> getKeyImages() {
return new ArrayList<String>(keyImages);
}
/**
* Set the key images to listen to.
*
* @return the key images to listen to
*/
public void setKeyImages(String... keyImages) {
synchronized (keyImages) {
this.keyImages.clear();
this.keyImages.addAll(Arrays.asList(keyImages));
refreshPolling();
}
}
/**
* Add a key image to listen to.
*
* @param keyImage - the key image to listen to
*/
public void addKeyImage(String keyImage) {
synchronized (keyImages) {
addKeyImages(keyImage);
refreshPolling();
}
}
/**
* Add key images to listen to.
*
* @param keyImages - key images to listen to
*/
public void addKeyImages(String... keyImages) {
synchronized (keyImages) {
for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) this.keyImages.add(keyImage);
refreshPolling();
}
}
/**
* Remove a key image to listen to.
*
* @param keyImage - the key image to unlisten to
*/
public void removeKeyImage(String keyImage) {
synchronized (keyImages) {
removeKeyImages(keyImage);
refreshPolling();
}
}
/**
* Remove key images to listen to.
*
* @param keyImages - key images to unlisten to
*/
public void removeKeyImages(String... keyImages) {
synchronized (keyImages) {
for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) throw new MoneroError("Key image not registered with poller: " + keyImage);
this.keyImages.removeAll(Arrays.asList(keyImages));
}
}
public void poll() {
synchronized (keyImages) {
// fetch spent statuses
List<MoneroKeyImageSpentStatus> spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages);
// collect changed statuses
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
for (int i = 0; i < keyImages.size(); i++) {
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
}
}
// announce changes
for (MoneroKeyImageListener listener : new ArrayList<MoneroKeyImageListener>(listeners)) listener.onSpentStatusChanged(changedStatuses);
}
}
private void refreshPolling() {
setIsPolling(listeners.size() > 0);
}
private void setIsPolling(boolean isPolling) {
if (isPolling) looper.start(refreshPeriodMs);
else looper.stop();
}
}

View file

@ -20,8 +20,10 @@ import bisq.core.trade.MakerTrade;
import bisq.core.trade.SellerTrade; import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
import com.google.common.collect.TreeMultimap;
import com.google.common.util.concurrent.Service.State; import com.google.common.util.concurrent.Service.State;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import common.utils.JsonUtils; import common.utils.JsonUtils;
@ -29,6 +31,7 @@ import java.io.File;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -47,6 +50,7 @@ import monero.common.MoneroRpcConnection;
import monero.common.MoneroRpcError; import monero.common.MoneroRpcError;
import monero.common.MoneroUtils; import monero.common.MoneroUtils;
import monero.daemon.MoneroDaemonRpc; import monero.daemon.MoneroDaemonRpc;
import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroNetworkType; import monero.daemon.model.MoneroNetworkType;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.daemon.model.MoneroSubmitTxResult; import monero.daemon.model.MoneroSubmitTxResult;
@ -97,6 +101,7 @@ public class XmrWalletService {
private TradeManager tradeManager; private TradeManager tradeManager;
private MoneroWalletRpc wallet; private MoneroWalletRpc wallet;
private Map<String, MoneroWallet> multisigWallets; private Map<String, MoneroWallet> multisigWallets;
private Map<String, Object> walletLocks = new HashMap<String, Object>();
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>(); private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
@Inject @Inject
@ -182,29 +187,40 @@ public class XmrWalletService {
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
} }
public boolean multisigWalletExists(String tradeId) { private synchronized void initWalletLock(String id) {
return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId); if (!walletLocks.containsKey(id)) walletLocks.put(id, new Object());
}
public boolean multisigWalletExists(String tradeId) {
initWalletLock(tradeId);
synchronized(walletLocks.get(tradeId)) {
return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId);
}
} }
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
public MoneroWallet createMultisigWallet(String tradeId) { public MoneroWallet createMultisigWallet(String tradeId) {
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId);
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); synchronized(walletLocks.get(tradeId)) {
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, false); // auto-assign port if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
multisigWallets.put(tradeId, multisigWallet); String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
return multisigWallet; MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port
multisigWallets.put(tradeId, multisigWallet);
return multisigWallet;
}
} }
// TODO (woodser): provide progress notifications during open? // TODO (woodser): provide progress notifications during open?
public MoneroWallet getMultisigWallet(String tradeId) { public MoneroWallet getMultisigWallet(String tradeId) {
log.info("{}.getMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId);
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); synchronized(walletLocks.get(tradeId)) {
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId); String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null); if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId);
multisigWallets.put(tradeId, multisigWallet); MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
return multisigWallet; multisigWallets.put(tradeId, multisigWallet);
return multisigWallet;
}
} }
public void saveWallet(MoneroWallet wallet) { public void saveWallet(MoneroWallet wallet) {
@ -213,19 +229,25 @@ public class XmrWalletService {
} }
public void closeMultisigWallet(String tradeId) { public void closeMultisigWallet(String tradeId) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId);
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); synchronized(walletLocks.get(tradeId)) {
MoneroWallet wallet = multisigWallets.remove(tradeId); log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
closeWallet(wallet, true); if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId);
MoneroWallet wallet = multisigWallets.remove(tradeId);
closeWallet(wallet, true);
}
} }
public boolean deleteMultisigWallet(String tradeId) { public boolean deleteMultisigWallet(String tradeId) {
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); initWalletLock(tradeId);
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; synchronized(walletLocks.get(tradeId)) {
if (!walletExists(walletName)) return false; log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); // TODO: synchronize String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
deleteWallet(walletName); if (!walletExists(walletName)) return false;
return true; if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId);
deleteWallet(walletName);
return true;
}
} }
public MoneroTxWallet createTx(List<MoneroDestination> destinations) { public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
@ -254,21 +276,20 @@ public class XmrWalletService {
// get expected mining fee // get expected mining fee
MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig() MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(TradeUtils.getTradeFeeAddress(), tradeFee) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount)); .addDestination(returnAddress, depositAmount));
BigInteger miningFee = miningFeeTx.getFee(); BigInteger miningFee = miningFeeTx.getFee();
// create reserve tx // create reserve tx
MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig() MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(TradeUtils.getTradeFeeAddress(), tradeFee) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit? .addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit?
// freeze inputs // freeze inputs
if (freezeInputs) { if (freezeInputs) {
for (MoneroOutput input : reserveTx.getInputs()) { for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
wallet.freezeOutput(input.getKeyImage().getHex()); wallet.save();
}
} }
return reserveTx; return reserveTx;
@ -291,13 +312,12 @@ public class XmrWalletService {
// create deposit tx // create deposit tx
MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig() MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(TradeUtils.getTradeFeeAddress(), tradeFee) .addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
.addDestination(multisigAddress, depositAmount)); .addDestination(multisigAddress, depositAmount));
// freeze deposit inputs // freeze deposit inputs
for (MoneroOutput input : depositTx.getInputs()) { for (MoneroOutput input : depositTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
wallet.freezeOutput(input.getKeyImage().getHex()); wallet.save();
}
return depositTx; return depositTx;
} }
@ -342,7 +362,7 @@ public class XmrWalletService {
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee // verify trade fee
String feeAddress = TradeUtils.getTradeFeeAddress(); String feeAddress = HavenoUtils.getTradeFeeAddress();
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee");
if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount());
@ -435,10 +455,8 @@ public class XmrWalletService {
// initialize main wallet if connected or previously created // initialize main wallet if connected or previously created
maybeInitMainWallet(); maybeInitMainWallet();
// update wallet connections on change // set and listen to daemon connection
connectionsService.addListener(newConnection -> { connectionsService.addListener(newConnection -> setDaemonConnection(newConnection));
setWalletDaemonConnections(newConnection);
});
} }
private boolean walletExists(String walletName) { private boolean walletExists(String walletName) {
@ -580,17 +598,13 @@ public class XmrWalletService {
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
} }
private void setWalletDaemonConnections(MoneroRpcConnection connection) { private void setDaemonConnection(MoneroRpcConnection connection) {
log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri())); log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri()));
if (wallet == null) maybeInitMainWallet(); if (wallet == null) maybeInitMainWallet();
if (wallet != null) { if (wallet != null) {
wallet.setDaemonConnection(connection); wallet.setDaemonConnection(connection);
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
} }
for (MoneroWallet multisigWallet : multisigWallets.values()) {
multisigWallet.setDaemonConnection(connection);
multisigWallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); // TODO: optimize when multisig wallets are open and syncing
}
} }
private void notifyBalanceListeners() { private void notifyBalanceListeners() {
@ -663,7 +677,7 @@ public class XmrWalletService {
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path); 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 + ".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); if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
deleteBackupWallets(walletName); deleteBackupWallets(walletName); // TODO: retain backup for some time?
} }
private void closeAllWallets() { private void closeAllWallets() {
@ -675,30 +689,16 @@ public class XmrWalletService {
openWallets.add(multisigWallets.get(multisigWalletKey)); openWallets.add(multisigWallets.get(multisigWalletKey));
} }
// done if no open wallets // close wallets in parallel
if (openWallets.isEmpty()) return; Set<Runnable> tasks = new HashSet<Runnable>();
for (MoneroWallet wallet : openWallets) tasks.add(() -> {
// close all wallets in parallel try {
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, openWallets.size())); closeWallet(wallet, true);
for (MoneroWallet openWallet : openWallets) { } catch (Exception e) {
pool.submit(new Runnable() { log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
@Override }
public void run() { });
try { HavenoUtils.awaitTasks(tasks);
closeWallet(openWallet, true);
} catch (Exception e) {
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
}
}
});
}
pool.shutdown();
try {
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
} catch (InterruptedException e) {
pool.shutdownNow();
throw new RuntimeException(e);
}
// clear wallets // clear wallets
wallet = null; wallet = null;

View file

@ -82,13 +82,11 @@ public class TradeEvents {
msg = Res.get("account.notifications.trade.message.msg.started", shortId); msg = Res.get("account.notifications.trade.message.msg.started", shortId);
break; break;
case PAYMENT_RECEIVED: case PAYMENT_RECEIVED:
break;
case PAYOUT_PUBLISHED:
// We only notify the buyer // We only notify the buyer
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing())) if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
msg = Res.get("account.notifications.trade.message.msg.completed", shortId); msg = Res.get("account.notifications.trade.message.msg.completed", shortId);
break; break;
case WITHDRAWN: case COMPLETED:
break; break;
} }
if (msg != null) { if (msg != null) {

View file

@ -23,12 +23,10 @@ import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.monetary.Price; import bisq.core.monetary.Price;
import bisq.core.offer.availability.DisputeAgentSelection;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.PaymentAccountUtil;
import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService; import bisq.core.provider.price.PriceFeedService;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
@ -48,9 +46,6 @@ import org.bitcoinj.core.Coin;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;

View file

@ -22,7 +22,7 @@ import bisq.core.filter.FilterManager;
import bisq.core.payment.PaymentAccount; import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.PaymentAccountUtil;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.user.User; import bisq.core.user.User;
@ -224,6 +224,6 @@ public class OfferFilterService {
public boolean hasValidSignature(Offer offer) { public boolean hasValidSignature(Offer offer) {
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()); Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
if (arbitrator == null) return false; // invalid arbitrator if (arbitrator == null) return false; // invalid arbitrator
return TradeUtils.isArbitratorSignatureValid(offer, arbitrator); return HavenoUtils.isArbitratorSignatureValid(offer, arbitrator);
} }
} }

View file

@ -36,7 +36,7 @@ import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.TradableList; import bisq.core.trade.TradableList;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.trade.handlers.TransactionResultHandler; import bisq.core.trade.handlers.TransactionResultHandler;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
@ -564,6 +564,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
if (offer.getOfferPayload().getReserveTxKeyImages() != null) { if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage); for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
xmrWalletService.getWallet().save();
} }
offer.setState(Offer.State.REMOVED); offer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED); openOffer.setState(OpenOffer.State.CANCELED);
@ -634,7 +635,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
latch.countDown(); latch.countDown();
errorMessages.add(errorMessage); errorMessages.add(errorMessage);
}); });
TradeUtils.awaitLatch(latch); HavenoUtils.awaitLatch(latch);
} }
requestPersistence(); requestPersistence();
if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString()); if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString());

View file

@ -21,7 +21,7 @@ import bisq.core.offer.AvailabilityResult;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.availability.OfferAvailabilityModel; import bisq.core.offer.availability.OfferAvailabilityModel;
import bisq.core.offer.messages.OfferAvailabilityResponse; import bisq.core.offer.messages.OfferAvailabilityResponse;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.common.taskrunner.Task; import bisq.common.taskrunner.Task;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
@ -54,7 +54,7 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
} }
// verify maker signature for trade request // verify maker signature for trade request
if (!TradeUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) { if (!HavenoUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) {
offer.setState(Offer.State.NOT_AVAILABLE); offer.setState(Offer.State.NOT_AVAILABLE);
failed("Take offer attempt failed because maker signature is invalid"); failed("Take offer attempt failed because maker signature is invalid");
return; return;

View file

@ -62,7 +62,7 @@ public class PlaceOfferProtocol {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void placeOffer() { public void placeOffer() {
log.debug("placeOffer() " + model.getOffer().getId()); log.info("{}.placeOffer() {}", getClass().getSimpleName(), model.getOffer().getId());
timeoutTimer = UserThread.runAfter(() -> { timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing")); handleError(Res.get("createOffer.timeoutAtPublishing"));

View file

@ -20,7 +20,7 @@ package bisq.core.offer.placeoffer.tasks;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.placeoffer.PlaceOfferModel; import bisq.core.offer.placeoffer.PlaceOfferModel;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -42,7 +42,7 @@ public class MakerProcessSignOfferResponse extends Task<PlaceOfferModel> {
Arbitrator arbitrator = checkNotNull(model.getUser().getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedArbitratorByAddress(arbitratorSigner) must not be null"); Arbitrator arbitrator = checkNotNull(model.getUser().getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedArbitratorByAddress(arbitratorSigner) must not be null");
// validate arbitrator signature // validate arbitrator signature
if (!TradeUtils.isArbitratorSignatureValid(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), arbitrator)) { if (!HavenoUtils.isArbitratorSignatureValid(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), arbitrator)) {
throw new RuntimeException("Offer payload has invalid arbitrator signature"); throw new RuntimeException("Offer payload has invalid arbitrator signature");
} }

View file

@ -39,22 +39,18 @@ import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.core.support.dispute.refund.refundagent.RefundAgent;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage;
import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage;
import bisq.core.trade.messages.PaymentAccountKeyRequest; import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.PaymentAccountKeyResponse;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.RefreshTradeStateRequest; import bisq.core.trade.messages.RefreshTradeStateRequest;
import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TraderSignedWitnessMessage; import bisq.core.trade.messages.TraderSignedWitnessMessage;
import bisq.core.trade.messages.UpdateMultisigRequest;
import bisq.core.trade.messages.UpdateMultisigResponse;
import bisq.network.p2p.AckMessage; import bisq.network.p2p.AckMessage;
import bisq.network.p2p.BundleOfEnvelopes; import bisq.network.p2p.BundleOfEnvelopes;
@ -158,21 +154,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
return DepositRequest.fromProto(proto.getDepositRequest(), this, messageVersion); return DepositRequest.fromProto(proto.getDepositRequest(), this, messageVersion);
case DEPOSIT_RESPONSE: case DEPOSIT_RESPONSE:
return DepositResponse.fromProto(proto.getDepositResponse(), this, messageVersion); return DepositResponse.fromProto(proto.getDepositResponse(), this, messageVersion);
case PAYMENT_ACCOUNT_KEY_REQUEST: case DEPOSITS_CONFIRMED_MESSAGE:
return PaymentAccountKeyRequest.fromProto(proto.getPaymentAccountKeyRequest(), this, messageVersion); return DepositsConfirmedMessage.fromProto(proto.getDepositsConfirmedMessage(), this, messageVersion);
case PAYMENT_ACCOUNT_KEY_RESPONSE:
return PaymentAccountKeyResponse.fromProto(proto.getPaymentAccountKeyResponse(), this, messageVersion);
case UPDATE_MULTISIG_REQUEST:
return UpdateMultisigRequest.fromProto(proto.getUpdateMultisigRequest(), this, messageVersion);
case UPDATE_MULTISIG_RESPONSE:
return UpdateMultisigResponse.fromProto(proto.getUpdateMultisigResponse(), this, messageVersion);
case PAYMENT_SENT_MESSAGE: case PAYMENT_SENT_MESSAGE:
return PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion); return PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion);
case PAYMENT_RECEIVED_MESSAGE: case PAYMENT_RECEIVED_MESSAGE:
return PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), messageVersion); return PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), messageVersion);
case PAYOUT_TX_PUBLISHED_MESSAGE:
return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion);
case TRADER_SIGNED_WITNESS_MESSAGE: case TRADER_SIGNED_WITNESS_MESSAGE:
return TraderSignedWitnessMessage.fromProto(proto.getTraderSignedWitnessMessage(), messageVersion); return TraderSignedWitnessMessage.fromProto(proto.getTraderSignedWitnessMessage(), messageVersion);

View file

@ -333,13 +333,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
if (isAgent(dispute)) { if (isAgent(dispute)) {
// update arbitrator's multisig wallet // update arbitrator's multisig wallet
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); trade.syncWallet();
multisigWallet.importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex()); trade.getWallet().importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex());
trade.saveWallet();
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId()); log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
// close multisig wallet
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
synchronized (disputeList) { synchronized (disputeList) {
if (!disputeList.contains(dispute)) { if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
@ -748,6 +745,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
chatMessage.getUid()); chatMessage.getUid());
// TODO: hack to sync wallet after dispute message received in order to detect payout published
Trade trade = tradeManager.getTrade(dispute.getTradeId());
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
for (int i = 0; i < 3; i++) {
UserThread.runAfter(() -> {
if (!trade.isPayoutUnlocked()) trade.syncWallet();
}, defaultRefreshPeriod / 1000 * (i + 1));
}
// We use the chatMessage wrapped inside the disputeResultMessage for // We use the chatMessage wrapped inside the disputeResultMessage for
// the state, as that is displayed to the user and we only persist that msg // the state, as that is displayed to the user and we only persist that msg
chatMessage.setArrived(true); chatMessage.setArrived(true);

View file

@ -301,7 +301,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.trace("We don't publish the tx as we are not the winning party."); log.trace("We don't publish the tx as we are not the winning party.");
// Clean up tangling trades // Clean up tangling trades
if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) { if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) {
updateTradeOrOpenOfferManager(tradeId); closeTradeOrOffer(tradeId);
} }
} }
} }
@ -324,7 +324,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
// we get a TransactionVerificationException. No reason to keep that dispute open... // we get a TransactionVerificationException. No reason to keep that dispute open...
updateTradeOrOpenOfferManager(tradeId); // TODO (woodser): only close in case of verification exception? closeTradeOrOffer(tradeId); // TODO (woodser): only close in case of verification exception?
throw new RuntimeException(errorMessage); throw new RuntimeException(errorMessage);
} finally { } finally {
@ -338,7 +338,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
synchronized (trade) { 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? 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.exportMultisigHex(), dispute, contract); sendArbitratorPayoutTxRequest(multisigWallet.exportMultisigHex(), dispute, contract);
xmrWalletService.closeMultisigWallet(tradeId);
} }
} }
} }
@ -376,12 +375,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
cleanupRetryMap(uid); cleanupRetryMap(uid);
// update multisig wallet // update trade wallet
if (xmrWalletService.multisigWalletExists(tradeId)) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion? MoneroWallet wallet = trade.getWallet();
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); if (wallet != null) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
multisigWallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex()); trade.syncWallet();
MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0); wallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex());
xmrWalletService.closeMultisigWallet(tradeId); trade.saveWallet();
MoneroTxWallet parsedPayoutTx = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash()); dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx); XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
} }
@ -397,6 +397,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} }
// Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published // Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published
// TODO: this should be invoked from mailbox message and send mailbox message response to support offline arbitrator
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) { private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName()); log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName());
String tradeId = request.getTradeId(); String tradeId = request.getTradeId();
@ -426,9 +427,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} }
// update arbitrator's multisig wallet with co-signer's multisig hex // update arbitrator's multisig wallet with co-signer's multisig hex
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); trade.syncWallet();
MoneroWallet multisigWallet = trade.getWallet();
try { try {
multisigWallet.importMultisigHex(request.getUpdatedMultisigHex()); multisigWallet.importMultisigHex(request.getUpdatedMultisigHex());
trade.saveWallet();
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId); log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
return; return;
@ -439,9 +442,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
System.out.println("Arbitrator created updated payout tx for co-signer!!!"); System.out.println("Arbitrator created updated payout tx for co-signer!!!");
System.out.println(payoutTx); System.out.println(payoutTx);
// close multisig wallet
xmrWalletService.closeMultisigWallet(tradeId);
// send updated payout tx to sender // send updated payout tx to sender
PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse( ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse(
@ -455,10 +455,19 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
senderPubKeyRing, senderPubKeyRing,
response, response,
new SendDirectMessageListener() { new SendDirectMessageListener() {
@Override @Override
public void onArrived() { public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, uid={}", log.info("{} arrived at peer {}. tradeId={}, uid={}",
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid()); response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
// TODO: hack to sync wallet after dispute message received in order to detect payout published
Trade trade = tradeManager.getTrade(dispute.getTradeId());
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
for (int i = 0; i < 3; i++) {
UserThread.runAfter(() -> {
if (!trade.isPayoutUnlocked()) trade.syncWallet();
}, defaultRefreshPeriod / 1000 * (i + 1));
}
} }
@Override @Override
@ -546,6 +555,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// update multisig wallet from arbitrator // update multisig wallet from arbitrator
multisigWallet.importMultisigHex(disputeResult.getArbitratorUpdatedMultisigHex()); multisigWallet.importMultisigHex(disputeResult.getArbitratorUpdatedMultisigHex());
xmrWalletService.saveWallet(multisigWallet);
// sign arbitrator-signed payout tx // sign arbitrator-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex); MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
@ -575,10 +585,10 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// update state // update state
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
trade.setPayoutTxId(txSet.getTxs().get(0).getHash()); trade.setPayoutTxId(txSet.getTxs().get(0).getHash());
trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); trade.setPayoutState(Trade.PayoutState.PUBLISHED);
dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash()); dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash());
sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract); sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract);
updateTradeOrOpenOfferManager(tradeId); closeTradeOrOffer(tradeId);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -623,7 +633,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
); );
} }
private void updateTradeOrOpenOfferManager(String tradeId) { public void closeTradeOrOffer(String tradeId) {
// set state after payout as we call swapTradeEntryToAvailableEntry // set state after payout as we call swapTradeEntryToAvailableEntry
if (tradeManager.getOpenTrade(tradeId).isPresent()) { if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED); tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
@ -632,7 +642,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
} }
} }
// dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx // dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx
private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) { private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) {
ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest( ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest(

View file

@ -64,7 +64,7 @@ public class TradeChatSession extends SupportSession {
@Override @Override
public boolean chatIsOpen() { public boolean chatIsOpen() {
return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED; return trade != null && trade.getState() != Trade.State.TRADE_COMPLETED;
} }
@Override @Override

View file

@ -158,7 +158,7 @@ public class ClosedTradableFormatter {
if (isBisqV1Trade(tradable)) { if (isBisqV1Trade(tradable)) {
Trade trade = castToTrade(tradable); Trade trade = castToTrade(tradable);
if (trade.isWithdrawn() || trade.isPayoutPublished()) { if (trade.isCompleted() || trade.isPayoutPublished()) {
return Res.get("portfolio.closed.completed"); return Res.get("portfolio.closed.completed");
} else if (trade.getDisputeState() == DISPUTE_CLOSED) { } else if (trade.getDisputeState() == DISPUTE_CLOSED) {
return Res.get("portfolio.closed.ticketClosed"); return Res.get("portfolio.closed.ticketClosed");

View file

@ -18,24 +18,24 @@
package bisq.core.trade; package bisq.core.trade;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig; import bisq.common.crypto.Sig;
import bisq.common.util.Tuple2;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferPayload;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.util.JsonUtil; import bisq.core.util.JsonUtil;
import java.net.URI; import java.net.URI;
import java.util.Objects; import java.util.Collection;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/** /**
* Collection of utilities for trading. * Collection of utilities.
*/ */
public class TradeUtils { public class HavenoUtils {
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
public static final String LOCALHOST = "localhost"; public static final String LOCALHOST = "localhost";
@ -148,61 +148,6 @@ public class TradeUtils {
return false; return false;
} }
} }
// TODO (woodser): remove the following utitilites?
// Returns <MULTI_SIG, TRADE_PAYOUT> if both are AVAILABLE, otherwise null
static Tuple2<String, String> getAvailableAddresses(Trade trade, XmrWalletService xmrWalletService,
KeyRing keyRing) {
var addresses = getTradeAddresses(trade, xmrWalletService, keyRing);
if (addresses == null)
return null;
if (xmrWalletService.getAvailableAddressEntries().stream()
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first)))
return null;
if (xmrWalletService.getAvailableAddressEntries().stream()
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second)))
return null;
return new Tuple2<>(addresses.first, addresses.second);
}
// Returns <MULTI_SIG, TRADE_PAYOUT> addresses as strings if they're known by the wallet
public static Tuple2<String, String> getTradeAddresses(Trade trade, XmrWalletService xmrWalletService,
KeyRing keyRing) {
var contract = trade.getContract();
if (contract == null)
return null;
// TODO (woodser): xmr multisig does not use pub key
throw new RuntimeException("need to replace btc multisig pub key with xmr");
// Get multisig address
// var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing());
// var multiSigPubKey = isMyRoleBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey();
// if (multiSigPubKey == null)
// return null;
// var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey);
// var multiSigAddress = xmrWalletService.getAddressEntryListAsImmutableList().stream()
// .filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString))
// .findAny()
// .orElse(null);
// if (multiSigAddress == null)
// return null;
//
// // Get payout address
// var payoutAddress = isMyRoleBuyer ?
// contract.getBuyerPayoutAddressString() : contract.getSellerPayoutAddressString();
// var payoutAddressEntry = xmrWalletService.getAddressEntryListAsImmutableList().stream()
// .filter(e -> Objects.equals(e.getAddressString(), payoutAddress))
// .findAny()
// .orElse(null);
// if (payoutAddressEntry == null)
// return null;
//
// return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress);
}
public static void awaitLatch(CountDownLatch latch) { public static void awaitLatch(CountDownLatch latch) {
try { try {
@ -211,4 +156,17 @@ public class TradeUtils {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
public static void awaitTasks(Collection<Runnable> tasks) {
if (tasks.isEmpty()) return;
ExecutorService pool = Executors.newFixedThreadPool(tasks.size());
for (Runnable task : tasks) pool.submit(task);
pool.shutdown();
try {
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
} catch (InterruptedException e) {
pool.shutdownNow();
throw new RuntimeException(e);
}
}
} }

View file

@ -50,9 +50,9 @@ import bisq.common.util.Utilities;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.Message; import com.google.protobuf.Message;
import common.utils.GenUtils;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction; import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
@ -88,13 +88,14 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.common.MoneroError; import monero.common.MoneroError;
import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon; import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroCheckTx;
import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxQuery;
import monero.wallet.model.MoneroTxSet; import monero.wallet.model.MoneroTxSet;
@ -125,9 +126,8 @@ public abstract class Trade implements Tradable, Model {
// deposit requested // deposit requested
SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), // not a mailbox msg, not used... remove
SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
// deposit published // deposit published
ARBITRATOR_PUBLISHED_DEPOSIT_TXS(Phase.DEPOSITS_PUBLISHED), ARBITRATOR_PUBLISHED_DEPOSIT_TXS(Phase.DEPOSITS_PUBLISHED),
@ -142,30 +142,20 @@ public abstract class Trade implements Tradable, Model {
// payment sent // payment sent
BUYER_CONFIRMED_IN_UI_PAYMENT_SENT(Phase.PAYMENT_SENT), BUYER_CONFIRMED_IN_UI_PAYMENT_SENT(Phase.PAYMENT_SENT),
BUYER_SENT_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), BUYER_SENT_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
BUYER_SEND_FAILED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), BUYER_SEND_FAILED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
SELLER_RECEIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT), SELLER_RECEIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
// payment received // payment received
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT(Phase.PAYMENT_RECEIVED), SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT(Phase.PAYMENT_RECEIVED),
SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
// payout published SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
SELLER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED), // TODO (woodser): this enum is over used, like during arbitration
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
BUYER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED),
PAYOUT_TX_SEEN_IN_NETWORK(Phase.PAYOUT_PUBLISHED),
// trade completed // trade completed
WITHDRAW_COMPLETED(Phase.WITHDRAWN); TRADE_COMPLETED(Phase.COMPLETED);
@NotNull @NotNull
public Phase getPhase() { public Phase getPhase() {
@ -199,14 +189,13 @@ public abstract class Trade implements Tradable, Model {
public enum Phase { public enum Phase {
INIT, INIT,
DEPOSIT_REQUESTED, // TODO (woodser): remove unused phases DEPOSIT_REQUESTED,
DEPOSITS_PUBLISHED, DEPOSITS_PUBLISHED,
DEPOSITS_CONFIRMED, DEPOSITS_CONFIRMED,
DEPOSITS_UNLOCKED, DEPOSITS_UNLOCKED,
PAYMENT_SENT, PAYMENT_SENT,
PAYMENT_RECEIVED, PAYMENT_RECEIVED,
PAYOUT_PUBLISHED, COMPLETED;
WITHDRAWN;
public static Trade.Phase fromProto(protobuf.Trade.Phase phase) { public static Trade.Phase fromProto(protobuf.Trade.Phase phase) {
return ProtoUtil.enumFromProto(Trade.Phase.class, phase.name()); return ProtoUtil.enumFromProto(Trade.Phase.class, phase.name());
@ -224,6 +213,25 @@ public abstract class Trade implements Tradable, Model {
} }
} }
public enum PayoutState {
UNPUBLISHED,
PUBLISHED,
CONFIRMED,
UNLOCKED;
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
}
public static protobuf.Trade.PayoutState toProtoMessage(Trade.PayoutState state) {
return protobuf.Trade.PayoutState.valueOf(state.name());
}
public boolean isValidTransitionTo(PayoutState newState) {
return newState.ordinal() > this.ordinal();
}
}
public enum DisputeState { public enum DisputeState {
NO_DISPUTE, NO_DISPUTE,
// arbitration // arbitration
@ -307,7 +315,6 @@ public abstract class Trade implements Tradable, Model {
private long takeOfferDate; private long takeOfferDate;
// Mutable // Mutable
@Nullable
@Getter @Getter
@Setter @Setter
private long amountAsLong; private long amountAsLong;
@ -317,6 +324,8 @@ public abstract class Trade implements Tradable, Model {
@Getter @Getter
private State state = State.PREPARATION; private State state = State.PREPARATION;
@Getter @Getter
private PayoutState payoutState = PayoutState.UNPUBLISHED;
@Getter
private DisputeState disputeState = DisputeState.NO_DISPUTE; private DisputeState disputeState = DisputeState.NO_DISPUTE;
@Getter @Getter
private TradePeriodState periodState = TradePeriodState.FIRST_HALF; private TradePeriodState periodState = TradePeriodState.FIRST_HALF;
@ -351,11 +360,17 @@ public abstract class Trade implements Tradable, Model {
transient final private XmrWalletService xmrWalletService; transient final private XmrWalletService xmrWalletService;
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state); transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
transient final private ObjectProperty<Phase> statePhaseProperty = new SimpleObjectProperty<>(state.phase); transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
transient final private ObjectProperty<PayoutState> payoutStateProperty = new SimpleObjectProperty<>(payoutState);
transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState); transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState);
transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState); transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState);
transient final private StringProperty errorMessageProperty = new SimpleStringProperty(); transient final private StringProperty errorMessageProperty = new SimpleStringProperty();
transient private Subscription tradePhaseSubscription = null;
transient private Subscription payoutStateSubscription = null;
transient private TaskLooper tradeTxsLooper;
transient private Long lastWalletRefreshPeriod;
private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour
// Mutable // Mutable
@Getter @Getter
transient private boolean isInitialized; transient private boolean isInitialized;
@ -530,6 +545,7 @@ public abstract class Trade implements Tradable, Model {
.setAmountAsLong(amountAsLong) .setAmountAsLong(amountAsLong)
.setPrice(price) .setPrice(price)
.setState(Trade.State.toProtoMessage(state)) .setState(Trade.State.toProtoMessage(state))
.setPayoutState(Trade.PayoutState.toProtoMessage(payoutState))
.setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState))
.setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState)) .setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState))
.addAllChatMessage(chatMessages.stream() .addAllChatMessage(chatMessages.stream()
@ -556,6 +572,7 @@ public abstract class Trade implements Tradable, Model {
public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) { public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) {
trade.setTakeOfferDate(proto.getTakeOfferDate()); trade.setTakeOfferDate(proto.getTakeOfferDate());
trade.setState(State.fromProto(proto.getState())); trade.setState(State.fromProto(proto.getState()));
trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState()));
trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState())); trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState()));
trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState())); trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState()));
trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId())); trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
@ -590,7 +607,56 @@ public abstract class Trade implements Tradable, Model {
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
}); });
isInitialized = true; isInitialized = true; // TODO: move to end?
// listen to daemon connection
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
// done if payout unlocked
if (isPayoutUnlocked()) return;
// handle trade state events
if (isDepositPublished()) listenToTradeTxs();
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
updateTxListenerRefreshPeriod();
if (isDepositPublished()) listenToTradeTxs();
if (isCompleted()) {
UserThread.execute(() -> {
if (tradePhaseSubscription != null) {
tradePhaseSubscription.unsubscribe();
tradePhaseSubscription = null;
}
});
}
});
// handle payout state events
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
updateTxListenerRefreshPeriod();
// cleanup when payout published
if (isPayoutPublished()) {
log.info("Payout published for {} {}", getClass().getSimpleName(), getId());
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // complete arbitrator trade when payout published
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
}
// cleanup when payout unlocks
if (isPayoutUnlocked()) {
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time?
deleteWallet();
if (tradeTxsLooper != null) {
tradeTxsLooper.stop();
tradeTxsLooper = null;
}
UserThread.execute(() -> {
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
payoutStateSubscription = null;
}
});
}
});
} }
@ -603,12 +669,12 @@ public abstract class Trade implements Tradable, Model {
} }
public NodeAddress getTradingPeerNodeAddress() { public NodeAddress getTradingPeerNodeAddress() {
return getTradingPeer() == null ? null : getTradingPeer().getNodeAddress(); return getTradingPeer() == null ? null : getTradingPeer().getNodeAddress();
} }
public NodeAddress getArbitratorNodeAddress() { public NodeAddress getArbitratorNodeAddress() {
return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); return getArbitrator() == null ? null : getArbitrator().getNodeAddress();
} }
/** /**
* Create a contract based on the current state. * Create a contract based on the current state.
@ -761,9 +827,8 @@ public abstract class Trade implements Tradable, Model {
// submit payout tx // submit payout tx
if (publish) { if (publish) {
multisigWallet.submitMultisigTxHex(payoutTxHex); multisigWallet.submitMultisigTxHex(payoutTxHex);
setState(isArbitrator() ? Trade.State.WITHDRAW_COMPLETED : isBuyer() ? Trade.State.BUYER_PUBLISHED_PAYOUT_TX : Trade.State.SELLER_PUBLISHED_PAYOUT_TX); setPayoutState(Trade.PayoutState.PUBLISHED);
} }
walletService.closeMultisigWallet(getId());
} }
/** /**
@ -771,7 +836,7 @@ public abstract class Trade implements Tradable, Model {
* *
* @param paymentAccountKey is the key to decrypt the payment account payload * @param paymentAccountKey is the key to decrypt the payment account payload
*/ */
public void decryptPeersPaymentAccountPayload(byte[] paymentAccountKey) { public void decryptPeerPaymentAccountPayload(byte[] paymentAccountKey) {
try { try {
// decrypt payment account payload // decrypt payment account payload
@ -792,139 +857,6 @@ public abstract class Trade implements Tradable, Model {
} }
} }
/**
* 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) {
log.warn("Trade {} already listening for deposit txs", getId());
return;
}
// get daemon and primary wallet
MoneroWallet havenoWallet = processModel.getXmrWalletService().getWallet();
// fetch deposit txs from daemon
List<MoneroTx> txs = xmrWalletService.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()));
// handle deposit txs seen
if (txs.size() == 2) {
setStateDepositsPublished();
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
// check if deposit txs unlocked
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) {
setStateDepositsConfirmed();
long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK;
if (havenoWallet.getHeight() >= unlockHeight) {
setStateDepositsUnlocked();
return;
}
}
}
// create block listener
depositTxListener = new MoneroWalletListener() {
Long unlockHeight = null;
@Override
public void onNewBlock(long height) {
// skip if no longer listening
if (depositTxListener == null) return;
// use latest height
height = havenoWallet.getHeight();
// skip if before unlock height
if (unlockHeight != null && height < unlockHeight) return;
// fetch txs from daemon
List<MoneroTx> txs = xmrWalletService.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()));
// skip if deposit txs not seen
if (txs.size() != 2) return;
setStateDepositsPublished();
// update deposit txs
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
// check if deposit txs confirmed and compute unlock height
if (txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed() && unlockHeight == null) {
log.info("Multisig deposits confirmed for trade {}", getId());
setStateDepositsConfirmed();
unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK;
}
// check if deposit txs unlocked
if (unlockHeight != null && height >= unlockHeight) {
log.info("Multisig deposits unlocked for trade {}", getId());
xmrWalletService.removeWalletListener(depositTxListener); // remove listener when notified
depositTxListener = null; // prevent re-applying trade state in subsequent requests
setStateDepositsUnlocked();
}
}
};
// register wallet listener
xmrWalletService.addWalletListener(depositTxListener);
}
public void listenForPayoutTx() {
log.info("Listening for payout tx for trade {}", getId());
// check if payout tx already seen
if (getState().ordinal() >= Trade.State.PAYOUT_TX_SEEN_IN_NETWORK.ordinal()) {
log.warn("We had a payout tx already set. tradeId={}, state={}", getId(), getState());
return;
}
// get payout address entry
Optional<XmrAddressEntry> optionalPayoutEntry = xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT);
if (!optionalPayoutEntry.isPresent()) throw new RuntimeException("Trade does not have address entry for payout");
XmrAddressEntry payoutEntry = optionalPayoutEntry.get();
// watch for payout tx on loop
new Thread(() -> { // TODO: use thread manager
boolean found = false;
while (!found) {
if (getPayoutTxKey() != null) {
// get txs to payout address
List<MoneroTxWallet> txs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
.setTransferQuery(new MoneroTransferQuery()
.setAccountIndex(0)
.setSubaddressIndex(payoutEntry.getSubaddressIndex())
.setIsIncoming(true)));
// check for payout tx
for (MoneroTxWallet tx : txs) {
MoneroCheckTx txCheck = xmrWalletService.getWallet().checkTxKey(tx.getHash(), getPayoutTxKey(), payoutEntry.getAddressString());
if (txCheck.isGood() && txCheck.receivedAmount.compareTo(new BigInteger("0")) > 0) {
found = true;
setPayoutTx(tx);
setStateIfValidTransitionTo(Trade.State.PAYOUT_TX_SEEN_IN_NETWORK);
return;
}
}
}
// wait to loop
GenUtils.waitFor(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
}
}).start();
}
@Nullable @Nullable
public MoneroTx getTakerDepositTx() { public MoneroTx getTakerDepositTx() {
String depositTxHash = getProcessModel().getTaker().getDepositTxHash(); String depositTxHash = getProcessModel().getTaker().getDepositTxHash();
@ -984,6 +916,30 @@ public abstract class Trade implements Tradable, Model {
} }
} }
public MoneroWallet getWallet() {
return xmrWalletService.multisigWalletExists(getId()) ? xmrWalletService.getMultisigWallet(getId()) : null;
}
public void syncWallet() {
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
getWallet().sync();
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
pollWallet();
}
public void saveWallet() {
xmrWalletService.saveWallet(getWallet());
}
public void deleteWallet() {
if (xmrWalletService.multisigWalletExists(getId())) xmrWalletService.deleteMultisigWallet(getId());
else log.warn("Multisig wallet to delete for trade {} does not exist", getId());
}
public void shutDown() {
isInitialized = false;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Model implementation // Model implementation
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -1055,7 +1011,37 @@ public abstract class Trade implements Tradable, Model {
this.state = state; this.state = state;
UserThread.execute(() -> { UserThread.execute(() -> {
stateProperty.set(state); stateProperty.set(state);
statePhaseProperty.set(state.getPhase()); phaseProperty.set(state.getPhase());
});
}
public void setStateIfProgress(State state) {
if (state.ordinal() > getState().ordinal()) setState(state);
}
public void setPayoutStateIfValidTransitionTo(PayoutState newPayoutState) {
if (payoutState.isValidTransitionTo(newPayoutState)) {
setPayoutState(newPayoutState);
} else {
log.warn("Payout state change is not getting applied because it would cause an invalid transition. " +
"Trade payout state={}, intended payout state={}", payoutState, newPayoutState);
}
}
public void setPayoutState(PayoutState payoutState) {
if (isInitialized) {
// We don't want to log at startup the setState calls from all persisted trades
log.info("Set new payout state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), payoutState);
}
if (payoutState.ordinal() < this.payoutState.ordinal()) {
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" +
"Old payout state is: " + this.state + ". New payout state is: " + payoutState;
log.warn(message);
}
this.payoutState = payoutState;
UserThread.execute(() -> {
payoutStateProperty.set(payoutState);
}); });
} }
@ -1264,7 +1250,7 @@ public abstract class Trade implements Tradable, Model {
return getState().getPhase().ordinal() == Phase.INIT.ordinal(); return getState().getPhase().ordinal() == Phase.INIT.ordinal();
} }
public boolean isTakerFeePublished() { public boolean isDepositRequested() {
return getState().getPhase().ordinal() >= Phase.DEPOSIT_REQUESTED.ordinal(); return getState().getPhase().ordinal() >= Phase.DEPOSIT_REQUESTED.ordinal();
} }
@ -1319,16 +1305,20 @@ public abstract class Trade implements Tradable, Model {
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal();
} }
public boolean isPayoutPublished() {
return getState().getPhase().ordinal() >= Phase.PAYOUT_PUBLISHED.ordinal() || isWithdrawn();
}
public boolean isCompleted() { public boolean isCompleted() {
return isPayoutPublished(); return getState().getPhase().ordinal() >= Phase.COMPLETED.ordinal();
} }
public boolean isWithdrawn() { public boolean isPayoutPublished() {
return getState().getPhase().ordinal() == Phase.WITHDRAWN.ordinal(); return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal();
}
public boolean isPayoutConfirmed() {
return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal();
}
public boolean isPayoutUnlocked() {
return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal();
} }
public ReadOnlyObjectProperty<State> stateProperty() { public ReadOnlyObjectProperty<State> stateProperty() {
@ -1336,7 +1326,11 @@ public abstract class Trade implements Tradable, Model {
} }
public ReadOnlyObjectProperty<Phase> statePhaseProperty() { public ReadOnlyObjectProperty<Phase> statePhaseProperty() {
return statePhaseProperty; return phaseProperty;
}
public ReadOnlyObjectProperty<PayoutState> payoutStateProperty() {
return payoutStateProperty;
} }
public ReadOnlyObjectProperty<DisputeState> disputeStateProperty() { public ReadOnlyObjectProperty<DisputeState> disputeStateProperty() {
@ -1439,6 +1433,98 @@ public abstract class Trade implements Tradable, Model {
return tradeVolumeProperty; return tradeVolumeProperty;
} }
private void listenToTradeTxs() {
if (tradeTxsLooper != null) return;
log.info("Listening for payout tx for {} {}", getClass().getSimpleName(), getId());
// poll wallet for tx state
pollWallet();
tradeTxsLooper = new TaskLooper(() -> {
try {
pollWallet();
} catch (Exception e) {
if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage());
}
});
tradeTxsLooper.start(getWalletRefreshPeriod());
}
private void pollWallet() {
// skip if payout unlocked
if (isPayoutUnlocked()) return;
// rescan spent if deposits unlocked
if (isDepositUnlocked()) getWallet().rescanSpent();
// get txs with outputs
List<MoneroTxWallet> txs = getWallet().getTxs(new MoneroTxQuery()
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
.setIncludeOutputs(true));
// check deposit txs
if (!isDepositUnlocked()) {
if (txs.size() == 2) {
setStateDepositsPublished();
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
// check if deposit txs confirmed
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();
}
}
// check payout tx
else {
// check if deposit txs spent (appears on payout published)
for (MoneroTxWallet tx : txs) {
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (Boolean.TRUE.equals(output.isSpent())) {
setPayoutStatePublished();
}
}
}
// check for outgoing txs (appears on payout confirmed)
List<MoneroTxWallet> outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true));
if (!outgoingTxs.isEmpty()) {
MoneroTxWallet payoutTx = outgoingTxs.get(0);
setPayoutTx(payoutTx);
setPayoutStatePublished();
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
}
}
}
private void setDaemonConnection(MoneroRpcConnection connection) {
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId());
multisigWallet.setDaemonConnection(connection);
multisigWallet.startSyncing(getWalletRefreshPeriod());
updateTxListenerRefreshPeriod();
}
private void updateTxListenerRefreshPeriod() {
long walletRefreshPeriod = getWalletRefreshPeriod();
if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return;
log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod);
lastWalletRefreshPeriod = walletRefreshPeriod;
if (tradeTxsLooper != null) {
tradeTxsLooper.stop();
tradeTxsLooper = null;
listenToTradeTxs();
}
}
private long getWalletRefreshPeriod() {
if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // arbitrator slows trade wallet after deposits confirm since messages are expected so this is only backup
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate
}
private void setStateDepositsPublished() { private void setStateDepositsPublished() {
if (!isDepositPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); if (!isDepositPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
} }
@ -1451,6 +1537,18 @@ public abstract class Trade implements Tradable, Model {
if (!isDepositUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); if (!isDepositUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
} }
private void setPayoutStatePublished() {
if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED);
}
private void setPayoutStateConfirmed() {
if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED);
}
private void setPayoutStateUnlocked() {
if (!isPayoutUnlocked()) setPayoutState(PayoutState.UNLOCKED);
}
@Override @Override
public String toString() { public String toString() {
return "Trade{" + return "Trade{" +
@ -1463,6 +1561,7 @@ public abstract class Trade implements Tradable, Model {
",\n tradeAmountAsLong=" + amountAsLong + ",\n tradeAmountAsLong=" + amountAsLong +
",\n tradePrice=" + price + ",\n tradePrice=" + price +
",\n state=" + state + ",\n state=" + state +
",\n payoutState=" + payoutState +
",\n disputeState=" + disputeState + ",\n disputeState=" + disputeState +
",\n tradePeriodState=" + periodState + ",\n tradePeriodState=" + periodState +
",\n contract=" + contract + ",\n contract=" + contract +
@ -1477,7 +1576,7 @@ public abstract class Trade implements Tradable, Model {
",\n takerFee=" + takerFee + ",\n takerFee=" + takerFee +
",\n xmrWalletService=" + xmrWalletService + ",\n xmrWalletService=" + xmrWalletService +
",\n stateProperty=" + stateProperty + ",\n stateProperty=" + stateProperty +
",\n statePhaseProperty=" + statePhaseProperty + ",\n statePhaseProperty=" + phaseProperty +
",\n disputeStateProperty=" + disputeStateProperty + ",\n disputeStateProperty=" + disputeStateProperty +
",\n tradePeriodStateProperty=" + tradePeriodStateProperty + ",\n tradePeriodStateProperty=" + tradePeriodStateProperty +
",\n errorMessageProperty=" + errorMessageProperty + ",\n errorMessageProperty=" + errorMessageProperty +

View file

@ -41,10 +41,8 @@ import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountKeyRequest;
import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.UpdateMultisigRequest;
import bisq.core.trade.protocol.ArbitratorProtocol; import bisq.core.trade.protocol.ArbitratorProtocol;
import bisq.core.trade.protocol.MakerProtocol; import bisq.core.trade.protocol.MakerProtocol;
import bisq.core.trade.protocol.ProcessModel; import bisq.core.trade.protocol.ProcessModel;
@ -66,7 +64,6 @@ import bisq.network.p2p.P2PService;
import bisq.network.p2p.network.TorNetworkNode; import bisq.network.p2p.network.TorNetworkNode;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import bisq.common.ClockWatcher; import bisq.common.ClockWatcher;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.FaultHandler; import bisq.common.handlers.FaultHandler;
@ -245,10 +242,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
handleDepositRequest((DepositRequest) networkEnvelope, peer); handleDepositRequest((DepositRequest) networkEnvelope, peer);
} else if (networkEnvelope instanceof DepositResponse) { } else if (networkEnvelope instanceof DepositResponse) {
handleDepositResponse((DepositResponse) networkEnvelope, peer); handleDepositResponse((DepositResponse) networkEnvelope, peer);
} else if (networkEnvelope instanceof PaymentAccountKeyRequest) {
handlePaymentAccountKeyRequest((PaymentAccountKeyRequest) networkEnvelope, peer);
} else if (networkEnvelope instanceof UpdateMultisigRequest) {
handleUpdateMultisigRequest((UpdateMultisigRequest) networkEnvelope, peer);
} }
} }
@ -284,6 +277,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
thawUnreservedOutputs(); thawUnreservedOutputs();
} }
public void shutDown() {
// collect trades to shutdown
Set<Trade> trades = new HashSet<Trade>();
trades.addAll(tradableList.getList());
trades.addAll(closedTradableManager.getClosedTrades());
trades.addAll(failedTradesManager.getObservableList());
// shut down trades in parallel
Set<Runnable> tasks = new HashSet<Runnable>();
for (Trade trade : trades) tasks.add(() -> {
try {
trade.shutDown();
} catch (Exception e) {
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
}
});
HavenoUtils.awaitTasks(tasks);
}
private void thawUnreservedOutputs() { private void thawUnreservedOutputs() {
if (xmrWalletService.getWallet() == null) return; if (xmrWalletService.getWallet() == null) return;
@ -301,13 +314,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Set<String> frozenKeyImages = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery() Set<String> frozenKeyImages = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery()
.setIsFrozen(true) .setIsFrozen(true)
.setIsSpent(false)) .setIsSpent(false))
.stream().map(output -> output.getKeyImage().getHex()) .stream()
.map(output -> output.getKeyImage().getHex())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
frozenKeyImages.removeAll(reservedKeyImages); frozenKeyImages.removeAll(reservedKeyImages);
for (String unreservedFrozenKeyImage : frozenKeyImages) { for (String unreservedFrozenKeyImage : frozenKeyImages) {
log.info("Thawing output which is not reserved for offer or trade: " + unreservedFrozenKeyImage); log.info("Thawing output which is not reserved for offer or trade: " + unreservedFrozenKeyImage);
xmrWalletService.getWallet().thawOutput(unreservedFrozenKeyImage); xmrWalletService.getWallet().thawOutput(unreservedFrozenKeyImage);
} }
xmrWalletService.getWallet().save();
} }
public TradeProtocol getTradeProtocol(Trade trade) { public TradeProtocol getTradeProtocol(Trade trade) {
@ -369,7 +384,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) { private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) {
tradeProtocol.initialize(processModelServiceProvider, this, trade.getOffer()); tradeProtocol.initialize(processModelServiceProvider, this, trade.getOffer());
trade.initialize(processModelServiceProvider);
requestPersistence(); // TODO requesting persistence twice with initPersistedTrade() requestPersistence(); // TODO requesting persistence twice with initPersistedTrade()
} }
@ -470,7 +484,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage); log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
maybeRemoveTrade(trade); removeTrade(trade);
}); });
requestPersistence(); requestPersistence();
@ -555,7 +569,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
log.warn("Maker error during trade initialization: " + errorMessage); log.warn("Maker error during trade initialization: " + errorMessage);
openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig
maybeRemoveTrade(trade); removeTrade(trade);
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
}); });
@ -658,45 +672,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer); ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer);
} }
private void handlePaymentAccountKeyRequest(PaymentAccountKeyRequest request, NodeAddress peer) {
log.info("Received PaymentAccountKeyRequest from {} with tradeId {} and uid {}", peer, request.getTradeId(), request.getUid());
try {
Validator.nonEmptyStringOf(request.getTradeId());
} catch (Throwable t) {
log.warn("Invalid PaymentAccountKeyRequest message " + request.toString());
return;
}
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
if (!tradeOptional.isPresent()) {
log.warn("No trade with id " + request.getTradeId());
return;
}
Trade trade = tradeOptional.get();
((ArbitratorProtocol) getTradeProtocol(trade)).handlePaymentAccountKeyRequest(request, peer);
}
private void handleUpdateMultisigRequest(UpdateMultisigRequest request, NodeAddress peer) {
log.info("Received UpdateMultisigRequest from {} with tradeId {} and uid {}", peer, request.getTradeId(), request.getUid());
try {
Validator.nonEmptyStringOf(request.getTradeId());
} catch (Throwable t) {
log.warn("Invalid UpdateMultisigRequest message " + request.toString());
return;
}
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 -> {
log.warn("Error handling UpdateMultisigRequest: " + errorMessage);
if (takeOfferRequestErrorMessageHandler != null)
takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
});
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Take offer // Take offer
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -777,7 +752,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}, errorMessage -> { }, errorMessage -> {
log.warn("Taker error during trade initialization: " + errorMessage); log.warn("Taker error during trade initialization: " + errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage);
maybeRemoveTrade(trade); removeTrade(trade);
}); });
requestPersistence(); requestPersistence();
} }
@ -830,8 +805,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades // If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades
public void onTradeCompleted(Trade trade) { public void onTradeCompleted(Trade trade) {
closedTradableManager.add(trade); closedTradableManager.add(trade);
trade.setState(Trade.State.WITHDRAW_COMPLETED); trade.setState(Trade.State.TRADE_COMPLETED);
maybeRemoveTrade(trade); removeTrade(trade);
// TODO The address entry should have been removed already. Check and if its the case remove that. // TODO The address entry should have been removed already. Check and if its the case remove that.
xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId()); xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId());
@ -899,7 +874,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published) // If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published)
// we move the trade to failedTradesManager // we move the trade to failedTradesManager
public void onMoveInvalidTradeToFailedTrades(Trade trade) { public void onMoveInvalidTradeToFailedTrades(Trade trade) {
maybeRemoveTrade(trade); removeTrade(trade);
failedTradesManager.add(trade); failedTradesManager.add(trade);
} }
@ -1050,34 +1025,28 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst();
} }
private synchronized void maybeRemoveTrade(Trade trade) { private synchronized void removeTrade(Trade trade) {
log.info("TradeManager.maybeRemoveTrade()"); log.info("TradeManager.removeTrade()");
synchronized(tradableList) { synchronized(tradableList) {
if (!tradableList.contains(trade)) return; if (!tradableList.contains(trade)) return;
// delete trade if not possibly funded // remove trade
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSIT_REQUESTED.ordinal() || trade.getPhase().ordinal() >= Trade.Phase.PAYOUT_PUBLISHED.ordinal()) { // TODO: delete after payout unlocked tradableList.remove(trade);
// remove trade // unreserve trade key images
tradableList.remove(trade); if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(keyImage);
// unreserve trade key images xmrWalletService.getWallet().save();
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) {
xmrWalletService.getWallet().thawOutput(keyImage);
}
}
// delete multisig wallet
deleteTradeWallet(trade);
// unregister and persist
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
requestPersistence();
} else {
log.warn("Not deleting trade " + trade.getId() + " because its trade wallet might be funded");
// TODO: schedule wallet for deletion after unlock
} }
// delete trade wallet if before funded
if (xmrWalletService.multisigWalletExists(trade.getId()) && !trade.isDepositRequested()) {
trade.deleteWallet();
}
// unregister and persist
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
requestPersistence();
} }
} }
@ -1094,9 +1063,4 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void onTradesChanged() { private void onTradesChanged() {
this.numPendingTrades.set(getObservableList().size()); this.numPendingTrades.set(getObservableList().size());
} }
private void deleteTradeWallet(Trade trade) {
if (xmrWalletService.multisigWalletExists(trade.getId())) xmrWalletService.deleteMultisigWallet(trade.getId());
else log.warn("Multisig wallet to delete for trade {} does not exist", trade.getId());
}
} }

View file

@ -21,7 +21,6 @@ import bisq.core.proto.CoreProtoResolver;
import bisq.network.p2p.DirectMessage; import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import com.google.protobuf.ByteString;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;

View file

@ -24,6 +24,8 @@ import bisq.network.p2p.NodeAddress;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import java.util.Optional; import java.util.Optional;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil; import bisq.common.proto.ProtoUtil;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -31,25 +33,24 @@ import lombok.Value;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Value @Value
public final class PaymentAccountKeyResponse extends TradeMailboxMessage implements DirectMessage { public final class DepositsConfirmedMessage extends TradeMailboxMessage implements DirectMessage {
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
private final PubKeyRing pubKeyRing; private final PubKeyRing pubKeyRing;
@Nullable @Nullable
private final byte[] paymentAccountKey; private final byte[] sellerPaymentAccountKey;
@Nullable @Nullable
private final String updatedMultisigHex; private final String updatedMultisigHex;
public PaymentAccountKeyResponse(String tradeId, public DepositsConfirmedMessage(String tradeId,
NodeAddress senderNodeAddress, NodeAddress senderNodeAddress,
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
String uid, String uid,
String messageVersion, @Nullable byte[] sellerPaymentAccountKey,
@Nullable byte[] paymentAccountKey,
@Nullable String updatedMultisigHex) { @Nullable String updatedMultisigHex) {
super(messageVersion, tradeId, uid); super(Version.getP2PMessageVersion(), tradeId, uid);
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.pubKeyRing = pubKeyRing; this.pubKeyRing = pubKeyRing;
this.paymentAccountKey = paymentAccountKey; this.sellerPaymentAccountKey = sellerPaymentAccountKey;
this.updatedMultisigHex = updatedMultisigHex; this.updatedMultisigHex = updatedMultisigHex;
} }
@ -60,34 +61,34 @@ public final class PaymentAccountKeyResponse extends TradeMailboxMessage impleme
@Override @Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.PaymentAccountKeyResponse.Builder builder = protobuf.PaymentAccountKeyResponse.newBuilder() protobuf.DepositsConfirmedMessage.Builder builder = protobuf.DepositsConfirmedMessage.newBuilder()
.setTradeId(tradeId) .setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setPubKeyRing(pubKeyRing.toProtoMessage()) .setPubKeyRing(pubKeyRing.toProtoMessage())
.setUid(uid); .setUid(uid);
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e))); Optional.ofNullable(sellerPaymentAccountKey).ifPresent(e -> builder.setSellerPaymentAccountKey(ByteString.copyFrom(e)));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
return getNetworkEnvelopeBuilder().setPaymentAccountKeyResponse(builder).build(); return getNetworkEnvelopeBuilder().setDepositsConfirmedMessage(builder).build();
} }
public static PaymentAccountKeyResponse fromProto(protobuf.PaymentAccountKeyResponse proto, public static DepositsConfirmedMessage fromProto(protobuf.DepositsConfirmedMessage proto,
CoreProtoResolver coreProtoResolver, CoreProtoResolver coreProtoResolver,
String messageVersion) { String messageVersion) {
return new PaymentAccountKeyResponse(proto.getTradeId(), return new DepositsConfirmedMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()), NodeAddress.fromProto(proto.getSenderNodeAddress()),
PubKeyRing.fromProto(proto.getPubKeyRing()), PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getUid(), proto.getUid(),
messageVersion, ProtoUtil.byteArrayOrNullFromProto(proto.getSellerPaymentAccountKey()),
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
} }
@Override @Override
public String toString() { public String toString() {
return "PaymentAccountKeyResponse {" + return "DepositsConfirmedMessage {" +
"\n senderNodeAddress=" + senderNodeAddress + "\n senderNodeAddress=" + senderNodeAddress +
",\n pubKeyRing=" + pubKeyRing + ",\n pubKeyRing=" + pubKeyRing +
",\n paymentAccountKey=" + paymentAccountKey + ",\n sellerPaymentAccountKey=" + sellerPaymentAccountKey +
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -1,77 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.messages;
import bisq.core.proto.CoreProtoResolver;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import lombok.EqualsAndHashCode;
import lombok.Value;
@EqualsAndHashCode(callSuper = true)
@Value
public final class PaymentAccountKeyRequest extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final PubKeyRing pubKeyRing;
public PaymentAccountKeyRequest(String tradeId,
NodeAddress senderNodeAddress,
PubKeyRing pubKeyRing,
String uid,
String messageVersion) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.pubKeyRing = pubKeyRing;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.PaymentAccountKeyRequest.Builder builder = protobuf.PaymentAccountKeyRequest.newBuilder()
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setPubKeyRing(pubKeyRing.toProtoMessage())
.setUid(uid);
return getNetworkEnvelopeBuilder().setPaymentAccountKeyRequest(builder).build();
}
public static PaymentAccountKeyRequest fromProto(protobuf.PaymentAccountKeyRequest proto,
CoreProtoResolver coreProtoResolver,
String messageVersion) {
return new PaymentAccountKeyRequest(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getUid(),
messageVersion);
}
@Override
public String toString() {
return "PaymentAccountKeyRequest {" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n pubKeyRing=" + pubKeyRing +
"\n} " + super.toString();
}
}

View file

@ -22,6 +22,7 @@ import bisq.core.account.sign.SignedWitness;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.proto.ProtoUtil;
import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.network.NetworkEnvelope;
import java.util.Optional; import java.util.Optional;
@ -38,7 +39,12 @@ import javax.annotation.Nullable;
@Value @Value
public final class PaymentReceivedMessage extends TradeMailboxMessage { public final class PaymentReceivedMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress; private final NodeAddress senderNodeAddress;
private final String payoutTxHex; @Nullable
private final String unsignedPayoutTxHex;
@Nullable
private final String signedPayoutTxHex;
private final String updatedMultisigHex;
private final boolean sawArrivedPaymentReceivedMsg;
// Added in v1.4.0 // Added in v1.4.0
@Nullable @Nullable
@ -47,13 +53,19 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
public PaymentReceivedMessage(String tradeId, public PaymentReceivedMessage(String tradeId,
NodeAddress senderNodeAddress, NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness, @Nullable SignedWitness signedWitness,
String signedPayoutTxHex) { String unsignedPayoutTxHex,
String signedPayoutTxHex,
String updatedMultisigHex,
boolean sawArrivedPaymentReceivedMsg) {
this(tradeId, this(tradeId,
senderNodeAddress, senderNodeAddress,
signedWitness, signedWitness,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Version.getP2PMessageVersion(), Version.getP2PMessageVersion(),
signedPayoutTxHex); unsignedPayoutTxHex,
signedPayoutTxHex,
updatedMultisigHex,
sawArrivedPaymentReceivedMsg);
} }
@ -66,11 +78,17 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
@Nullable SignedWitness signedWitness, @Nullable SignedWitness signedWitness,
String uid, String uid,
String messageVersion, String messageVersion,
String signedPayoutTxHex) { String unsignedPayoutTxHex,
String signedPayoutTxHex,
String updatedMultisigHex,
boolean sawArrivedPaymentReceivedMsg) {
super(messageVersion, tradeId, uid); super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.signedWitness = signedWitness; this.signedWitness = signedWitness;
this.payoutTxHex = signedPayoutTxHex; this.unsignedPayoutTxHex = unsignedPayoutTxHex;
this.signedPayoutTxHex = signedPayoutTxHex;
this.updatedMultisigHex = updatedMultisigHex;
this.sawArrivedPaymentReceivedMsg = sawArrivedPaymentReceivedMsg;
} }
@Override @Override
@ -79,8 +97,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
.setTradeId(tradeId) .setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid) .setUid(uid)
.setPayoutTxHex(payoutTxHex); .setSawArrivedPaymentReceivedMsg(sawArrivedPaymentReceivedMsg);
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness())); Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex));
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build(); return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
} }
@ -96,7 +117,10 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
signedWitness, signedWitness,
proto.getUid(), proto.getUid(),
messageVersion, messageVersion,
proto.getPayoutTxHex()); ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
proto.getSawArrivedPaymentReceivedMsg());
} }
@Override @Override
@ -104,7 +128,10 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
return "SellerReceivedPaymentMessage{" + return "SellerReceivedPaymentMessage{" +
"\n senderNodeAddress=" + senderNodeAddress + "\n senderNodeAddress=" + senderNodeAddress +
",\n signedWitness=" + signedWitness + ",\n signedWitness=" + signedWitness +
",\n payoutTxHex=" + payoutTxHex + ",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex +
",\n signedPayoutTxHex=" + signedPayoutTxHex +
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +
",\n sawArrivedPaymentReceivedMsg=" + sawArrivedPaymentReceivedMsg +
"\n} " + super.toString(); "\n} " + super.toString();
} }
} }

View file

@ -52,8 +52,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
@Nullable String counterCurrencyTxId, @Nullable String counterCurrencyTxId,
@Nullable String counterCurrencyExtraData, @Nullable String counterCurrencyExtraData,
String uid, String uid,
String signedPayoutTxHex, @Nullable String signedPayoutTxHex,
String updatedMultisigHex, @Nullable String updatedMultisigHex,
@Nullable byte[] paymentAccountKey) { @Nullable byte[] paymentAccountKey) {
this(tradeId, this(tradeId,
senderNodeAddress, senderNodeAddress,

View file

@ -1,118 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.messages;
import bisq.core.account.sign.SignedWitness;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.Optional;
import java.util.UUID;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
@EqualsAndHashCode(callSuper = true)
@Value
public final class PayoutTxPublishedMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress;
private final boolean isMaker;
private final String signedPayoutTxHex;
// Added in v1.4.0
@Nullable
private final SignedWitness signedWitness;
public PayoutTxPublishedMessage(String tradeId,
NodeAddress senderNodeAddress,
boolean isMaker,
@Nullable SignedWitness signedWitness,
String signedPayoutTxHex) {
this(tradeId,
senderNodeAddress,
isMaker,
signedWitness,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
signedPayoutTxHex);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private PayoutTxPublishedMessage(String tradeId,
NodeAddress senderNodeAddress,
boolean isMaker,
@Nullable SignedWitness signedWitness,
String uid,
String messageVersion,
String signedPayoutTxHex) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.isMaker = isMaker;
this.signedWitness = signedWitness;
this.signedPayoutTxHex = signedPayoutTxHex;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.PayoutTxPublishedMessage.Builder builder = protobuf.PayoutTxPublishedMessage.newBuilder()
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setIsMaker(isMaker)
.setUid(uid)
.setSignedPayoutTxHex(signedPayoutTxHex);
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
return getNetworkEnvelopeBuilder().setPayoutTxPublishedMessage(builder).build();
}
public static NetworkEnvelope fromProto(protobuf.PayoutTxPublishedMessage proto, String messageVersion) {
// There is no method to check for a nullable non-primitive data type object but we know that all fields
// are empty/null, so we check for the signature to see if we got a valid signedWitness.
protobuf.SignedWitness protoSignedWitness = proto.getSignedWitness();
SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ?
SignedWitness.fromProto(protoSignedWitness) :
null;
return new PayoutTxPublishedMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getIsMaker(),
signedWitness,
proto.getUid(),
messageVersion,
proto.getSignedPayoutTxHex());
}
@Override
public String toString() {
return "PayoutTxPublishedMessage{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n isMaker=" + isMaker +
",\n signedWitness=" + signedWitness +
",\n signedPayoutTxHex=" + signedPayoutTxHex +
"\n} " + super.toString();
}
}

View file

@ -1,99 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.messages;
import bisq.core.proto.CoreProtoResolver;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Value;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Value
public final class UpdateMultisigRequest extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final PubKeyRing pubKeyRing;
private final long currentDate;
@Nullable
private final String updatedMultisigHex;
public UpdateMultisigRequest(String tradeId,
NodeAddress senderNodeAddress,
PubKeyRing pubKeyRing,
String uid,
String messageVersion,
long currentDate,
String updatedMultisigHex) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.pubKeyRing = pubKeyRing;
this.currentDate = currentDate;
this.updatedMultisigHex = updatedMultisigHex;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.UpdateMultisigRequest.Builder builder = protobuf.UpdateMultisigRequest.newBuilder()
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setPubKeyRing(pubKeyRing.toProtoMessage())
.setUid(uid);
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
builder.setCurrentDate(currentDate);
return getNetworkEnvelopeBuilder().setUpdateMultisigRequest(builder).build();
}
public static UpdateMultisigRequest fromProto(protobuf.UpdateMultisigRequest proto,
CoreProtoResolver coreProtoResolver,
String messageVersion) {
return new UpdateMultisigRequest(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getUid(),
messageVersion,
proto.getCurrentDate(),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
}
@Override
public String toString() {
return "UpdateMultisigRequest {" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n pubKeyRing=" + pubKeyRing +
",\n currentDate=" + currentDate +
",\n updatedMultisigHex='" + updatedMultisigHex +
"\n} " + super.toString();
}
}

View file

@ -1,99 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.messages;
import bisq.core.proto.CoreProtoResolver;
import bisq.network.p2p.DirectMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Value;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Value
public final class UpdateMultisigResponse extends TradeMessage implements DirectMessage {
private final NodeAddress senderNodeAddress;
private final PubKeyRing pubKeyRing;
private final long currentDate;
@Nullable
private final String updatedMultisigHex;
public UpdateMultisigResponse(String tradeId,
NodeAddress senderNodeAddress,
PubKeyRing pubKeyRing,
String uid,
String messageVersion,
long currentDate,
String updatedMultisigHex) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.pubKeyRing = pubKeyRing;
this.currentDate = currentDate;
this.updatedMultisigHex = updatedMultisigHex;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.UpdateMultisigResponse.Builder builder = protobuf.UpdateMultisigResponse.newBuilder()
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setPubKeyRing(pubKeyRing.toProtoMessage())
.setUid(uid);
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
builder.setCurrentDate(currentDate);
return getNetworkEnvelopeBuilder().setUpdateMultisigResponse(builder).build();
}
public static UpdateMultisigResponse fromProto(protobuf.UpdateMultisigResponse proto,
CoreProtoResolver coreProtoResolver,
String messageVersion) {
return new UpdateMultisigResponse(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
PubKeyRing.fromProto(proto.getPubKeyRing()),
proto.getUid(),
messageVersion,
proto.getCurrentDate(),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
}
@Override
public String toString() {
return "UpdateMultisigResponse {" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n pubKeyRing=" + pubKeyRing +
",\n currentDate=" + currentDate +
",\n updatedMultisigHex='" + updatedMultisigHex +
"\n} " + super.toString();
}
}

View file

@ -6,18 +6,16 @@ import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositRequest; import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountKeyRequest;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.ArbitratorProcessDepositRequest; import bisq.core.trade.protocol.tasks.ArbitratorProcessDepositRequest;
import bisq.core.trade.protocol.tasks.ArbitratorProcessPaymentAccountKeyRequest;
import bisq.core.trade.protocol.tasks.ArbitratorProcessReserveTx; import bisq.core.trade.protocol.tasks.ArbitratorProcessReserveTx;
import bisq.core.trade.protocol.tasks.ArbitratorProcessPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.ArbitratorSendInitTradeOrMultisigRequests; import bisq.core.trade.protocol.tasks.ArbitratorSendInitTradeOrMultisigRequests;
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest; import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.Validator; import bisq.core.util.Validator;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -32,17 +30,11 @@ public class ArbitratorProtocol extends DisputeProtocol {
@Override @Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) { protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer); super.onTradeMessage(message, peer);
if (message instanceof PayoutTxPublishedMessage) {
handle((PayoutTxPublishedMessage) message, peer);
}
} }
@Override @Override
public void onMailboxMessage(TradeMessage message, NodeAddress peer) { public void onMailboxMessage(TradeMessage message, NodeAddress peer) {
super.onMailboxMessage(message, peer); super.onMailboxMessage(message, peer);
if (message instanceof PayoutTxPublishedMessage) {
handle((PayoutTxPublishedMessage) message, peer);
}
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -118,57 +110,10 @@ public class ArbitratorProtocol extends DisputeProtocol {
public void handleDepositResponse(DepositResponse response, NodeAddress sender) { public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
log.warn("Arbitrator ignoring DepositResponse for trade " + response.getTradeId()); log.warn("Arbitrator ignoring DepositResponse for trade " + response.getTradeId());
} }
public void handlePaymentAccountKeyRequest(PaymentAccountKeyRequest request, NodeAddress sender) { @SuppressWarnings("unchecked")
System.out.println("ArbitratorProtocol.handlePaymentAccountKeyRequest() " + trade.getId()); @Override
new Thread(() -> { public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
synchronized (trade) { return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class };
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);
expect(new Condition(trade)
.with(request)
.from(sender))
.setup(tasks(
ArbitratorProcessPaymentAccountKeyRequest.class)
.using(new TradeTaskRunner(trade,
() -> {
stopTimeout();
handleTaskRunnerSuccess(sender, request);
},
errorMessage -> {
handleTaskRunnerFault(sender, request, errorMessage);
}))
.withTimeout(TRADE_TIMEOUT))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
}
protected void handle(PayoutTxPublishedMessage request, NodeAddress peer) {
System.out.println("ArbitratorProtocol.handle(PayoutTxPublishedMessage)");
new Thread(() -> {
synchronized (trade) {
if (trade.isCompleted()) return; // ignore subsequent requests
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), request);
processModel.setTradeMessage(request);
expect(anyPhase(Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED)
.with(request)
.from(peer))
.setup(tasks(
ArbitratorProcessPayoutTxPublishedMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(peer, request);
},
errorMessage -> {
handleTaskRunnerFault(peer, request, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
} }
} }

View file

@ -22,7 +22,7 @@ import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountKeyResponse; import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
@ -31,7 +31,7 @@ import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import bisq.common.taskrunner.Task;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@ -102,7 +102,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
} }
@Override @Override
public void handle(PaymentAccountKeyResponse request, NodeAddress sender) { public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
super.handle(request, sender); super.handle(request, sender);
} }

View file

@ -24,7 +24,7 @@ import bisq.core.trade.Trade;
import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.PaymentAccountKeyResponse; import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
@ -113,7 +113,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
} }
@Override @Override
public void handle(PaymentAccountKeyResponse request, NodeAddress sender) { public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
super.handle(request, sender); super.handle(request, sender);
} }

View file

@ -17,35 +17,23 @@
package bisq.core.trade.protocol; package bisq.core.trade.protocol;
import bisq.common.UserThread;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import bisq.core.trade.BuyerTrade; import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentAccountKeyResponse;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentAccountKeyResponse;
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentAccountKeyRequestToArbitrator;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
import bisq.core.trade.protocol.tasks.BuyerSendPayoutTxPublishedMessage; import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener; import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener;
import bisq.core.util.Validator;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
@Slf4j @Slf4j
public abstract class BuyerProtocol extends DisputeProtocol { public class BuyerProtocol extends DisputeProtocol {
private boolean listeningToSendPaymentAccountKey;
private boolean paymentAccountPayloadKeyRequestSent;
enum BuyerEvent implements FluentProtocol.Event { enum BuyerEvent implements FluentProtocol.Event {
STARTUP, STARTUP,
DEPOSIT_TXS_CONFIRMED, DEPOSIT_TXS_CONFIRMED,
@ -66,23 +54,8 @@ public abstract class BuyerProtocol extends DisputeProtocol {
// TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades // TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades
// request key to decrypt seller's payment account payload after first confirmation
sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent.STARTUP, false);
// listen for deposit txs
given(anyPhase(Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED)
.with(BuyerEvent.STARTUP))
.setup(tasks(SetupDepositTxsListener.class))
.executeTasks();
// listen for payout tx
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
.with(BuyerEvent.STARTUP))
.setup(tasks(SetupPayoutTxListener.class))
.executeTasks();
// send payment sent message // send payment sent message
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) // TODO: remove payment received phase? given(anyPhase(Trade.Phase.PAYMENT_SENT)
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG) .anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)
.with(BuyerEvent.STARTUP)) .with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSendPaymentSentMessage.class)) .setup(tasks(BuyerSendPaymentSentMessage.class))
@ -92,49 +65,16 @@ public abstract class BuyerProtocol extends DisputeProtocol {
@Override @Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) { protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer); super.onTradeMessage(message, peer);
if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peer);
} if (message instanceof PaymentAccountKeyResponse) {
handle((PaymentAccountKeyResponse) message, peer);
}
} }
@Override @Override
public void onMailboxMessage(TradeMessage message, NodeAddress peer) { public void onMailboxMessage(TradeMessage message, NodeAddress peer) {
super.onMailboxMessage(message, peer); super.onMailboxMessage(message, peer);
if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peer);
} else if (message instanceof PaymentAccountKeyResponse) {
handle((PaymentAccountKeyResponse) message, peer);
}
} }
@Override @Override
public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) { public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) {
super.handleSignContractResponse(response, sender); super.handleSignContractResponse(response, sender);
sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent.DEPOSIT_TXS_CONFIRMED, true);
}
public void handle(PaymentAccountKeyResponse response, NodeAddress sender) {
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountKeyResponse()");
new Thread(() -> {
synchronized (trade) {
latchTrade();
expect(new Condition(trade)
.with(response)
.from(sender))
.setup(tasks(BuyerProcessPaymentAccountKeyResponse.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(sender, response);
},
errorMessage -> {
handleTaskRunnerFault(sender, response, errorMessage);
})))
.executeTasks();
awaitTradeLatch();
}
}).start();
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -177,84 +117,9 @@ public abstract class BuyerProtocol extends DisputeProtocol {
}).start(); }).start();
} }
/////////////////////////////////////////////////////////////////////////////////////////// @SuppressWarnings("unchecked")
// Incoming message Payout tx @Override
/////////////////////////////////////////////////////////////////////////////////////////// public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
System.out.println("BuyerProtocol.handle(PaymentReceivedMessage)");
new Thread(() -> {
synchronized (trade) {
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
.with(message)
.from(peer))
.setup(tasks(
BuyerProcessPaymentReceivedMessage.class,
BuyerSendPayoutTxPublishedMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
}
private void sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent event, boolean waitForSellerOnConfirm) {
// skip if payment account payload already decrypted or not enough progress
if (trade.getSeller().getPaymentAccountPayload() != null) return;
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSIT_REQUESTED.ordinal()) return;
// if confirmed and waiting for seller, recheck later
if (trade.getState() == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN && waitForSellerOnConfirm) {
UserThread.runAfter(() -> {
sendPaymentAccountKeyRequestIfWhenNeeded(event, false);
}, TRADE_TIMEOUT);
return;
}
// else if confirmed send request and return
else if (trade.getState().ordinal() >= Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN.ordinal()) {
sendPaymentAccountKeyRequest(event);
return;
}
// register for state changes once
if (!listeningToSendPaymentAccountKey) {
listeningToSendPaymentAccountKey = true;
EasyBind.subscribe(trade.stateProperty(), state -> {
sendPaymentAccountKeyRequestIfWhenNeeded(event, waitForSellerOnConfirm);
});
}
}
private void sendPaymentAccountKeyRequest(BuyerEvent event) {
new Thread(() -> {
synchronized (trade) {
if (paymentAccountPayloadKeyRequestSent) return;
if (trade.getSeller().getPaymentAccountPayload() != null) return; // skip if initialized
latchTrade();
expect(new Condition(trade))
.setup(tasks(BuyerSendPaymentAccountKeyRequestToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(event);
},
(errorMessage) -> {
handleTaskRunnerFault(event, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
paymentAccountPayloadKeyRequestSent = true;
}
}).start();
} }
} }

View file

@ -22,25 +22,20 @@ import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.BuyerProtocol.BuyerEvent;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.SellerMaybeSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage; import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator;
import bisq.core.trade.protocol.tasks.SellerSendPaymentAccountPayloadKey; import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.network.p2p.NodeAddress; import bisq.network.p2p.NodeAddress;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
@Slf4j @Slf4j
public abstract class SellerProtocol extends DisputeProtocol { public class SellerProtocol extends DisputeProtocol {
enum SellerEvent implements FluentProtocol.Event { enum SellerEvent implements FluentProtocol.Event {
STARTUP, STARTUP,
DEPOSIT_TXS_CONFIRMED, DEPOSIT_TXS_CONFIRMED,
@ -54,25 +49,6 @@ public abstract class SellerProtocol extends DisputeProtocol {
@Override @Override
protected void onInitialized() { protected void onInitialized() {
super.onInitialized(); super.onInitialized();
// TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades
// send payment account payload key when trade state is confirmed
if (trade.getPhase() == Trade.Phase.DEPOSIT_REQUESTED || trade.getPhase() == Trade.Phase.DEPOSITS_PUBLISHED) {
sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent.STARTUP);
}
// listen for deposit txs
given(anyPhase(Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED)
.with(SellerEvent.STARTUP))
.setup(tasks(SetupDepositTxsListener.class))
.executeTasks();
// listen for payout tx
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
.with(BuyerEvent.STARTUP))
.setup(tasks(SetupPayoutTxListener.class))
.executeTasks();
} }
@Override @Override
@ -94,7 +70,6 @@ public abstract class SellerProtocol extends DisputeProtocol {
@Override @Override
public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) { public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) {
sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent.DEPOSIT_TXS_CONFIRMED);
super.handleSignContractResponse(response, sender); super.handleSignContractResponse(response, sender);
} }
@ -163,8 +138,8 @@ public abstract class SellerProtocol extends DisputeProtocol {
.setup(tasks( .setup(tasks(
ApplyFilter.class, ApplyFilter.class,
SellerPreparePaymentReceivedMessage.class, SellerPreparePaymentReceivedMessage.class,
SellerMaybeSendPayoutTxPublishedMessage.class, SellerSendPaymentReceivedMessageToBuyer.class,
SellerSendPaymentReceivedMessage.class) SellerSendPaymentReceivedMessageToArbitrator.class)
.using(new TradeTaskRunner(trade, () -> { .using(new TradeTaskRunner(trade, () -> {
this.errorMessageHandler = null; this.errorMessageHandler = null;
handleTaskRunnerSuccess(event); handleTaskRunnerSuccess(event);
@ -183,26 +158,9 @@ public abstract class SellerProtocol extends DisputeProtocol {
}).start(); }).start();
} }
private void sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent event) { @SuppressWarnings("unchecked")
EasyBind.subscribe(trade.stateProperty(), state -> { @Override
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) { public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
new Thread(() -> { return new Class[] { SendDepositsConfirmedMessageToBuyer.class };
synchronized (trade) {
latchTrade();
expect(new Condition(trade))
.setup(tasks(SellerSendPaymentAccountPayloadKey.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(event);
},
(errorMessage) -> {
handleTaskRunnerFault(event, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
}
});
} }
} }

View file

@ -18,24 +18,30 @@
package bisq.core.trade.protocol; package bisq.core.trade.protocol;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.trade.handlers.TradeResultHandler; import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse; import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse; import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage; import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.messages.UpdateMultisigRequest;
import bisq.core.trade.protocol.tasks.RemoveOffer; import bisq.core.trade.protocol.tasks.RemoveOffer;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.FluentProtocol.Condition;
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest; import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
import bisq.core.trade.protocol.tasks.ProcessDepositResponse; import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest; import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.ProcessSignContractRequest; import bisq.core.trade.protocol.tasks.ProcessSignContractRequest;
import bisq.core.trade.protocol.tasks.ProcessSignContractResponse; import bisq.core.trade.protocol.tasks.ProcessSignContractResponse;
import bisq.core.trade.protocol.tasks.ProcessUpdateMultisigRequest;
import bisq.core.util.Validator; import bisq.core.util.Validator;
import bisq.network.p2p.AckMessage; import bisq.network.p2p.AckMessage;
@ -47,7 +53,6 @@ import bisq.network.p2p.SendMailboxMessageListener;
import bisq.network.p2p.mailbox.MailboxMessage; import bisq.network.p2p.mailbox.MailboxMessage;
import bisq.network.p2p.mailbox.MailboxMessageService; import bisq.network.p2p.mailbox.MailboxMessageService;
import bisq.network.p2p.messaging.DecryptedMailboxListener; import bisq.network.p2p.messaging.DecryptedMailboxListener;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
@ -58,7 +63,6 @@ import bisq.common.taskrunner.Task;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
@ -93,10 +97,20 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) {
log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
if (message instanceof DepositsConfirmedMessage) {
handle((DepositsConfirmedMessage) message, peerNodeAddress);
} else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peerNodeAddress);
}
} }
protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid()); log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
if (message instanceof DepositsConfirmedMessage) {
handle((DepositsConfirmedMessage) message, peerNodeAddress);
} else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peerNodeAddress);
}
} }
@ -110,20 +124,22 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
protected void onInitialized() { protected void onInitialized() {
if (!trade.isWithdrawn()) { if (!trade.isCompleted()) {
processModel.getP2PService().addDecryptedDirectMessageListener(this); processModel.getP2PService().addDecryptedDirectMessageListener(this);
} }
// handle trade events
EasyBind.subscribe(trade.stateProperty(), state -> {
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage();
});
// initialize trade
trade.initialize(processModel.getProvider());
// process mailbox messages
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
// We delay a bit here as the trade gets updated from the wallet to update the trade mailboxMessageService.addDecryptedMailboxListener(this);
// state (deposit confirmed) and that happens after our method is called. handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
// TODO To fix that in a better way we would need to change the order of some routines
// from the TradeManager, but as we are close to a release I dont want to risk a bigger
// change and leave that for a later PR
UserThread.runAfter(() -> {
mailboxMessageService.addDecryptedMailboxListener(this);
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
}, 100, TimeUnit.MILLISECONDS);
} }
public void onWithdrawCompleted() { public void onWithdrawCompleted() {
@ -196,7 +212,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
TradeMessage tradeMessage = (TradeMessage) mailboxMessage; TradeMessage tradeMessage = (TradeMessage) mailboxMessage;
// We only remove here if we have already completed the trade. // We only remove here if we have already completed the trade.
// Otherwise removal is done after successfully applied the task runner. // Otherwise removal is done after successfully applied the task runner.
if (trade.isWithdrawn()) { if (trade.isCompleted()) {
processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(mailboxMessage); processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(mailboxMessage);
log.info("Remove {} from the P2P network as trade is already completed.", log.info("Remove {} from the P2P network as trade is already completed.",
tradeMessage.getClass().getSimpleName()); tradeMessage.getClass().getSimpleName());
@ -205,7 +221,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress());
} else if (mailboxMessage instanceof AckMessage) { } else if (mailboxMessage instanceof AckMessage) {
AckMessage ackMessage = (AckMessage) mailboxMessage; AckMessage ackMessage = (AckMessage) mailboxMessage;
if (!trade.isWithdrawn()) { if (!trade.isCompleted()) {
// We only apply the msg if we have not already completed the trade // We only apply the msg if we have not already completed the trade
onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress());
} }
@ -227,8 +243,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// Abstract // Abstract
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public abstract Class<? extends TradeTask>[] getDepsitsConfirmedTasks();
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()"); System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()");
new Thread(() -> { new Thread(() -> {
synchronized (trade) { synchronized (trade) {
latchTrade(); latchTrade();
@ -256,7 +274,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest() " + trade.getId()); System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() " + trade.getId());
new Thread(() -> { new Thread(() -> {
synchronized (trade) { synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
@ -292,7 +310,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId()); System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() " + trade.getId());
new Thread(() -> { new Thread(() -> {
synchronized (trade) { synchronized (trade) {
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
@ -329,7 +347,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleDepositResponse(DepositResponse response, NodeAddress sender) { public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()"); System.out.println(getClass().getSimpleName() + ".handleDepositResponse()");
new Thread(() -> { new Thread(() -> {
synchronized (trade) { synchronized (trade) {
latchTrade(); latchTrade();
@ -358,25 +376,55 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}).start(); }).start();
} }
// TODO (woodser): update to use fluent for consistency public void handle(DepositsConfirmedMessage response, NodeAddress sender) {
public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage)");
latchTrade(); new Thread(() -> {
Validator.checkTradeId(processModel.getOfferId(), message); synchronized (trade) {
processModel.setTradeMessage(message); latchTrade();
TradeTaskRunner taskRunner = new TradeTaskRunner(trade, expect(new Condition(trade)
() -> { .with(response)
stopTimeout(); .from(sender))
handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest"); .setup(tasks(ProcessDepositsConfirmedMessage.class)
}, .using(new TradeTaskRunner(trade,
errorMessage -> { () -> {
handleTaskRunnerFault(peer, message, errorMessage); handleTaskRunnerSuccess(sender, response);
}); },
taskRunner.addTasks( errorMessage -> {
ProcessUpdateMultisigRequest.class handleTaskRunnerFault(sender, response, errorMessage);
); })))
startTimeout(TRADE_TIMEOUT); .executeTasks();
taskRunner.run(); awaitTradeLatch();
awaitTradeLatch(); }
}).start();
}
// received by buyer and arbitrator
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
return;
}
if (trade instanceof ArbitratorTrade && !trade.isPayoutUnlocked()) trade.syncWallet(); // arbitrator syncs slowly after deposits confirmed
synchronized (trade) {
latchTrade();
Validator.checkTradeId(processModel.getOfferId(), message);
processModel.setTradeMessage(message);
expect(anyPhase(trade instanceof ArbitratorTrade ? new Trade.Phase[] { Trade.Phase.DEPOSITS_UNLOCKED } : new Trade.Phase[] { Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED })
.with(message)
.from(peer))
.setup(tasks(
ProcessPaymentReceivedMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(peer, message);
},
errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -591,7 +639,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) { protected void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) {
log.info("TaskRunner successfully completed. Triggered from {}, tradeId={}", source, trade.getId()); log.info("TaskRunner successfully completed. Triggered from {}, tradeId={}", source, trade.getId());
if (message != null) { if (message != null) {
sendAckMessage(sender, message, true, null); sendAckMessage(sender, message, true, null);
@ -638,7 +686,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected void awaitTradeLatch() { protected void awaitTradeLatch() {
if (tradeLatch == null) return; if (tradeLatch == null) return;
TradeUtils.awaitLatch(tradeLatch); HavenoUtils.awaitLatch(tradeLatch);
} }
private boolean isMyMessage(NetworkEnvelope message) { private boolean isMyMessage(NetworkEnvelope message) {
@ -653,4 +701,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
return false; return false;
} }
} }
private void sendDepositsConfirmedMessage() {
new Thread(() -> {
synchronized (trade) {
latchTrade();
expect(new Condition(trade))
.setup(tasks(getDepsitsConfirmedTasks())
.using(new TradeTaskRunner(trade,
() -> {
handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages");
},
(errorMessage) -> {
handleTaskRunnerFault(null, null, errorMessage);
})))
.executeTasks(true);
awaitTradeLatch();
}
}).start();
}
} }

View file

@ -36,7 +36,6 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroTxWallet;
import javax.annotation.Nullable; import javax.annotation.Nullable;
// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not

View file

@ -1,81 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentAccountKeyResponse;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ArbitratorProcessPaymentAccountKeyRequest extends TradeTask {
@SuppressWarnings({"unused"})
public ArbitratorProcessPaymentAccountKeyRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// ensure deposit txs confirmed
trade.listenForDepositTxs();
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
throw new RuntimeException("Arbitrator refusing payment account key request for trade " + trade.getId() + " because the deposit txs have not confirmed");
}
// create response for buyer with key to decrypt seller's payment account payload
PaymentAccountKeyResponse response = new PaymentAccountKeyResponse(
trade.getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
trade.getSeller().getPaymentAccountKey(),
null
);
// send response to buyer
NodeAddress buyerAddress = trade.getBuyer().getNodeAddress();
log.info("Arbitrator sending PaymentAccountKeyResponse to buyer={}; offerId={}", buyerAddress, trade.getId());
processModel.getP2PService().sendEncryptedDirectMessage(buyerAddress, trade.getBuyer().getPubKeyRing(), response, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), buyerAddress, trade.getId());
complete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), buyerAddress, trade.getId(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage);
failed();
}
});
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -1,57 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ArbitratorProcessPayoutTxPublishedMessage extends TradeTask {
@SuppressWarnings({"unused"})
public ArbitratorProcessPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
PayoutTxPublishedMessage request = (PayoutTxPublishedMessage) processModel.getTradeMessage();
// verify and publish payout tx
trade.verifyPayoutTx(request.getSignedPayoutTxHex(), false, true);
// update latest peer address
if (request.isMaker()) trade.getMaker().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
else trade.getTaker().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
// TODO: publish signed witness data?
//request.getSignedWitness()
// close arbitrator trade
processModel.getTradeManager().onTradeCompleted(trade);
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -63,24 +63,25 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
// create payout tx if we have seller's updated multisig hex // import multisig hex
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) { List<String> updatedMultisigHexes = new ArrayList<String>();
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
if (!updatedMultisigHexes.isEmpty()) {
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
trade.saveWallet();
}
// create payout tx // create payout tx if we have seller's updated multisig hex
if (trade.getSeller().getUpdatedMultisigHex() != null) {
// create payout tx
log.info("Buyer creating unsigned payout tx"); log.info("Buyer creating unsigned payout tx");
multisigWallet.importMultisigHex(trade.getTradingPeer().getUpdatedMultisigHex());
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx); trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
// start listening for published payout tx
trade.listenForPayoutTx();
} else {
if (trade.getSelf().getUpdatedMultisigHex() == null) trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); // only export multisig hex once
} }
// close multisig wallet
walletService.closeMultisigWallet(trade.getId());
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View file

@ -1,60 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentAccountKeyResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BuyerProcessPaymentAccountKeyResponse extends TradeTask {
@SuppressWarnings({"unused"})
public BuyerProcessPaymentAccountKeyResponse(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// update peer node address if not from arbitrator
if (!processModel.getTempTradingPeerNodeAddress().equals(trade.getArbitrator().getNodeAddress())) {
trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
}
// decrypt peer's payment account payload
PaymentAccountKeyResponse request = (PaymentAccountKeyResponse) processModel.getTradeMessage();
if (trade.getTradingPeer().getPaymentAccountPayload() == null) {
trade.decryptPeersPaymentAccountPayload(request.getPaymentAccountKey());
}
// store updated multisig hex for processing on payment sent
if (request.getUpdatedMultisigHex() != null) trade.getTradingPeer().setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// persist and complete
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -1,73 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.app.Version;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountKeyRequest;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BuyerSendPaymentAccountKeyRequestToArbitrator extends TradeTask {
@SuppressWarnings({"unused"})
public BuyerSendPaymentAccountKeyRequestToArbitrator(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// create request to arbitrator
PaymentAccountKeyRequest request = new PaymentAccountKeyRequest(
trade.getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion()
);
// send request to arbitrator
log.info("Sending {} with offerId {} and uid {} to arbitrator {} with pub key ring {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getPubKeyRing());
processModel.getP2PService().sendEncryptedDirectMessage(
trade.getArbitrator().getNodeAddress(),
trade.getArbitrator().getPubKeyRing(),
request,
new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at arbitrator: offerId={}", PaymentAccountKeyRequest.class.getSimpleName(), trade.getId());
complete();
}
@Override
public void onFault(String errorMessage) {
log.warn("Failed to send {} to arbitrator, error={}.", PaymentAccountKeyRequest.class.getSimpleName(), errorMessage);
failed();
}
});
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -63,7 +63,7 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
trade.getCounterCurrencyExtraData(), trade.getCounterCurrencyExtraData(),
deterministicId, deterministicId,
trade.getPayoutTxHex(), trade.getPayoutTxHex(),
trade.getBuyer().getUpdatedMultisigHex(), trade.getSelf().getUpdatedMultisigHex(),
trade.getSelf().getPaymentAccountKey() trade.getSelf().getPaymentAccountKey()
); );
} }

View file

@ -1,82 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.payment.PaymentAccount;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.network.p2p.NodeAddress;
import java.util.UUID;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class BuyerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
public BuyerSendPayoutTxPublishedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected NodeAddress getReceiverNodeAddress() {
return trade.getArbitrator().getNodeAddress();
}
@Override
protected PubKeyRing getReceiverPubKeyRing() {
return trade.getArbitrator().getPubKeyRing();
}
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
return new PayoutTxPublishedMessage(
tradeId,
processModel.getMyNodeAddress(),
trade.isMaker(),
null, // TODO: send witness data?
trade.getPayoutTxHex()
);
}
@Override
protected void setStateSent() {
log.info("Buyer sent PayoutTxPublishedMessage: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
@Override
protected void setStateArrived() {
log.info("Buyer's PayoutTxPublishedMessage arrived: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
@Override
protected void setStateStoredInMailbox() {
log.info("Buyer's PayoutTxPublishedMessage stored in mailbox: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
@Override
protected void setStateFault() {
log.error("Buyer's PayoutTxPublishedMessage failed: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
}

View file

@ -74,7 +74,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
// create deposit tx and freeze inputs // create deposit tx and freeze inputs
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade); MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade);
// TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig // TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig
// save process state // save process state

View file

@ -0,0 +1,68 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.protocol.TradingPeer;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ProcessDepositsConfirmedMessage extends TradeTask {
@SuppressWarnings({"unused"})
public ProcessDepositsConfirmedMessage(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// get sender based on the pub key
// TODO: trade.getTradingPeer(PubKeyRing)
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
TradingPeer sender;
if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator();
else if (trade.getBuyer().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getBuyer();
else if (trade.getSeller().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getSeller();
else throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
// update peer node address
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress());
// decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey());
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
}
// store updated multisig hex for processing on payment sent
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// persist and complete
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -122,7 +122,7 @@ public class ProcessInitMultisigRequest extends TradeTask {
log.info("Importing exchanged multisig hex for trade {}", trade.getId()); log.info("Importing exchanged multisig hex for trade {}", trade.getId());
MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getExchangedMultisigHex(), peers[1].getExchangedMultisigHex()), xmrWalletService.getWalletPassword()); MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getExchangedMultisigHex(), peers[1].getExchangedMultisigHex()), xmrWalletService.getWalletPassword());
processModel.setMultisigAddress(result.getAddress()); processModel.setMultisigAddress(result.getAddress());
processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); // save and close multisig wallet once it's created processModel.getProvider().getXmrWalletService().saveWallet(multisigWallet); // save multisig wallet once it's created
trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED);
} }

View file

@ -22,7 +22,7 @@ import bisq.core.offer.Offer;
import bisq.core.trade.ArbitratorTrade; import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.MakerTrade; import bisq.core.trade.MakerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.TradingPeer;
@ -71,7 +71,7 @@ public class ProcessInitTradeRequest extends TradeTask {
if (!trade.getTaker().getNodeAddress().equals(request.getTakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree"); if (!trade.getTaker().getNodeAddress().equals(request.getTakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree");
if (trade.getTaker().getPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest"); if (trade.getTaker().getPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest");
trade.getTaker().setPubKeyRing(request.getPubKeyRing()); trade.getTaker().setPubKeyRing(request.getPubKeyRing());
if (!TradeUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature if (!HavenoUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature
// check trade price // check trade price
try { try {

View file

@ -19,26 +19,22 @@ package bisq.core.trade.protocol.tasks;
import bisq.core.account.sign.SignedWitness; import bisq.core.account.sign.SignedWitness;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.util.Validator; import bisq.core.util.Validator;
import common.utils.GenUtils;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
@Slf4j @Slf4j
public class BuyerProcessPaymentReceivedMessage extends TradeTask { public class ProcessPaymentReceivedMessage extends TradeTask {
public BuyerProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) { public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
} }
@ -50,39 +46,44 @@ public class BuyerProcessPaymentReceivedMessage extends TradeTask {
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage(); PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
Validator.checkTradeId(processModel.getOfferId(), message); Validator.checkTradeId(processModel.getOfferId(), message);
checkNotNull(message); checkNotNull(message);
checkArgument(message.getPayoutTxHex() != null); checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
// update to the latest peer address of our peer if the message is correct // update to the latest peer address of our peer if the message is correct
trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests sometimes reuse addresses
// handle if payout tx is not seen on network // handle if payout tx not published
if (trade.getPhase().ordinal() < Trade.Phase.PAYOUT_PUBLISHED.ordinal()) { if (!trade.isPayoutPublished()) {
// publish payout tx if signed. otherwise verify, sign, and publish payout tx // import multisig hex
boolean previouslySigned = trade.getPayoutTxHex() != null; MoneroWallet multisigWallet = trade.getWallet();
if (previouslySigned) { if (message.getUpdatedMultisigHex() != null) {
log.info("Buyer publishing signed payout tx from seller"); multisigWallet.importMultisigHex(message.getUpdatedMultisigHex());
XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); trade.saveWallet();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
List<String> txHashes = multisigWallet.submitMultisigTxHex(message.getPayoutTxHex());
trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0)));
XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx());
trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG);
walletService.closeMultisigWallet(trade.getId());
} else {
log.info("Buyer verifying, signing, and publishing seller's payout tx");
trade.verifyPayoutTx(message.getPayoutTxHex(), true, true);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_PUBLISHED_PAYOUT_TX);
// TODO (woodser): send PayoutTxPublishedMessage to seller
} }
// mark address entries as available // arbitrator waits for buyer to sign and broadcast payout tx if message arrived
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId()); boolean isSigned = message.getSignedPayoutTxHex() != null;
if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) {
log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName());
GenUtils.waitFor(30000);
multisigWallet.rescanSpent();
}
// verify and publish payout tx
if (!trade.isPayoutPublished()) {
if (isSigned) {
log.info("{} publishing signed payout tx from seller", trade.getClass().getSimpleName());
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
} else {
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
}
}
} else { } else {
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); log.info("We got the payout tx already set from the payout listener and do nothing here. trade ID={}", trade.getId());
} }
// TODO: remove witness
SignedWitness signedWitness = message.getSignedWitness(); SignedWitness signedWitness = message.getSignedWitness();
if (signedWitness != null) { if (signedWitness != null) {
// We received the signedWitness from the seller and publish the data to the network. // We received the signedWitness from the seller and publish the data to the network.
@ -91,6 +92,8 @@ public class BuyerProcessPaymentReceivedMessage extends TradeTask {
processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness); processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness);
} }
// complete
if (!trade.isArbitrator()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator trade completes on payout published
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {

View file

@ -72,9 +72,6 @@ public class ProcessSignContractResponse extends TradeTask {
// send deposit request when all contract signatures received // send deposit request when all contract signatures received
if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
// start listening for deposit txs
trade.listenForDepositTxs();
// create request for arbitrator to deposit funds to multisig // create request for arbitrator to deposit funds to multisig
DepositRequest request = new DepositRequest( DepositRequest request = new DepositRequest(
trade.getOffer().getId(), trade.getOffer().getId(),

View file

@ -1,109 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.UpdateMultisigRequest;
import bisq.core.trade.messages.UpdateMultisigResponse;
import bisq.network.p2p.SendDirectMessageListener;
import bisq.common.app.Version;
import bisq.common.taskrunner.TaskRunner;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import static bisq.core.util.Validator.checkTradeId;
import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
@Slf4j
public class ProcessUpdateMultisigRequest extends TradeTask {
@SuppressWarnings({"unused"})
public ProcessUpdateMultisigRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
log.debug("current trade state " + trade.getState());
UpdateMultisigRequest request = (UpdateMultisigRequest) processModel.getTradeMessage();
checkNotNull(request);
checkTradeId(processModel.getOfferId(), request);
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId());
System.out.println("PROCESS UPDATE MULTISIG REQUEST");
System.out.println(request);
// check if multisig wallet needs updated
if (!multisigWallet.isMultisigImportNeeded()) {
log.warn("Multisig wallet does not need updated, so request is unexpected");
failed(); // TODO (woodser): ignore instead fail
return;
}
// get updated multisig hex
multisigWallet.sync();
String updatedMultisigHex = multisigWallet.exportMultisigHex();
// import the multisig hex
int numOutputsSigned = multisigWallet.importMultisigHex(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(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
updatedMultisigHex);
log.info("Send {} with offerId {} and uid {} to peer {}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid(), trade.getTradingPeer().getNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), response, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at trading peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid());
complete();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), response.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage);
appendToErrorMessage("Sending response failed: response=" + response + "\nerrorMessage=" + errorMessage);
failed();
}
});
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -1,97 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.network.p2p.NodeAddress;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class SellerMaybeSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
public SellerMaybeSendPayoutTxPublishedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// skip if payout tx not published
if (trade.getPhase().ordinal() < Trade.Phase.PAYOUT_PUBLISHED.ordinal()) {
complete();
return;
}
super.run();
} catch (Throwable t) {
failed(t);
}
}
@Override
protected NodeAddress getReceiverNodeAddress() {
return trade.getArbitrator().getNodeAddress();
}
@Override
protected PubKeyRing getReceiverPubKeyRing() {
return trade.getArbitrator().getPubKeyRing();
}
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
return new PayoutTxPublishedMessage(
tradeId,
processModel.getMyNodeAddress(),
trade.isMaker(),
null, // TODO: send witness data?
trade.getPayoutTxHex()
);
}
@Override
protected void setStateSent() {
log.info("Seller sent PayoutTxPublishedMessage: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
@Override
protected void setStateArrived() {
log.info("Seller's PayoutTxPublishedMessage arrived: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
@Override
protected void setStateStoredInMailbox() {
log.info("Seller's PayoutTxPublishedMessage stored in mailbox: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
@Override
protected void setStateFault() {
log.error("Seller's PayoutTxPublishedMessage failed: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
}
}

View file

@ -17,11 +17,16 @@
package bisq.core.trade.protocol.tasks; package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import java.util.ArrayList;
import java.util.List;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@Slf4j @Slf4j
@ -37,25 +42,35 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
// import multisig hex
MoneroWallet multisigWallet = trade.getWallet();
List<String> updatedMultisigHexes = new ArrayList<String>();
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
if (!updatedMultisigHexes.isEmpty()) {
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0]));
trade.saveWallet();
}
// verify, sign, and publish payout tx if given. otherwise create payout tx // verify, sign, and publish payout tx if given. otherwise create payout tx
if (trade.getPayoutTxHex() != null) { if (trade.getPayoutTxHex() != null) {
log.info("Seller verifying, signing, and publishing payout tx"); log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true); trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true);
// mark address entries as available
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId());
} else { } else {
// create unsigned payout tx // create unsigned payout tx
log.info("Seller creating unsigned payout tx"); log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
System.out.println("created payout tx: " + payoutTx);
trade.setPayoutTx(payoutTx); trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
// start listening for published payout tx // export multisig hex once
trade.listenForPayoutTx(); if (trade.getSelf().getUpdatedMultisigHex() == null) {
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
}
} }
processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View file

@ -20,12 +20,10 @@ package bisq.core.trade.protocol.tasks;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage; import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.util.Validator; import bisq.core.util.Validator;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
@Slf4j @Slf4j
public class SellerProcessPaymentSentMessage extends TradeTask { public class SellerProcessPaymentSentMessage extends TradeTask {
@ -47,18 +45,10 @@ public class SellerProcessPaymentSentMessage extends TradeTask {
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
// decrypt buyer's payment account payload // decrypt buyer's payment account payload
trade.decryptPeersPaymentAccountPayload(message.getPaymentAccountKey()); trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
// sync and update multisig wallet
if (trade.getBuyer().getUpdatedMultisigHex() != null) {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // TODO: ensure sync() always called before importMultisigHex()
multisigWallet.importMultisigHex(trade.getBuyer().getUpdatedMultisigHex());
walletService.closeMultisigWallet(trade.getId());
}
// update latest peer address // update latest peer address
trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress()); trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
String counterCurrencyTxId = message.getCounterCurrencyTxId(); String counterCurrencyTxId = message.getCounterCurrencyTxId();
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) { if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) {
@ -73,7 +63,6 @@ public class SellerProcessPaymentSentMessage extends TradeTask {
trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View file

@ -21,6 +21,8 @@ import bisq.core.account.sign.SignedWitness;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentReceivedMessage; import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.TradeMailboxMessage; import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -30,13 +32,17 @@ import static com.google.common.base.Preconditions.checkNotNull;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
public class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
SignedWitness signedWitness = null; SignedWitness signedWitness = null;
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) { public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
} }
protected abstract NodeAddress getReceiverNodeAddress();
protected abstract PubKeyRing getReceiverPubKeyRing();
@Override @Override
protected void run() { protected void run() {
try { try {
@ -55,47 +61,52 @@ public class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
} }
@Override @Override
protected TradeMailboxMessage getTradeMailboxMessage(String id) { protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null"); checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
// TODO: sign witness
// AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
// if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
// // Broadcast is done in accountAgeWitness domain.
// accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
// }
return new PaymentReceivedMessage( return new PaymentReceivedMessage(
id, tradeId,
processModel.getMyNodeAddress(), processModel.getMyNodeAddress(),
signedWitness, signedWitness,
trade.getPayoutTxHex() trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
trade.getSelf().getUpdatedMultisigHex(),
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout
); );
} }
// TODO: using PAYOUT_TX_PUBLISHED_MSG to represent PAYMENT_RECEIVED_MSG after payout, but PAYOUT_TX_PUBLISHED_MSG is specifically for arbitrator. delete *PAYOUT_TX_PUBLISHED* messages and check payout field manually?
@Override @Override
protected void setStateSent() { protected void setStateSent() {
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
log.info("Sent SellerReceivedPaymentMessage: tradeId={} at peer {} SignedWitness {}", log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG);
log.info("Seller's PaymentReceivedMessage arrived: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG);
log.info("Seller's PaymentReceivedMessage stored in mailbox: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
} }
@Override @Override
protected void setStateFault() { protected void setStateFault() {
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); trade.setStateIfProgress(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG);
log.error("SellerReceivedPaymentMessage failed: tradeId={} at peer {} SignedWitness {}", log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness); processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.setStateIfProgress(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG);
log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setStateIfProgress(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG);
log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
} }
} }

View file

@ -17,26 +17,27 @@
package bisq.core.trade.protocol.tasks; package bisq.core.trade.protocol.tasks;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@Slf4j @Slf4j
public class SetupDepositTxsListener extends TradeTask { public class SellerSendPaymentReceivedMessageToArbitrator extends SellerSendPaymentReceivedMessage {
@SuppressWarnings({ "unused" }) public SellerSendPaymentReceivedMessageToArbitrator(TaskRunner<Trade> taskHandler, Trade trade) {
public SetupDepositTxsListener(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
} }
@Override protected NodeAddress getReceiverNodeAddress() {
protected void run() { return trade.getArbitrator().getNodeAddress();
try { }
runInterceptHook();
trade.listenForDepositTxs(); protected PubKeyRing getReceiverPubKeyRing() {
complete(); return trade.getArbitrator().getPubKeyRing();
} catch (Throwable t) {
failed(t);
}
} }
} }

View file

@ -0,0 +1,52 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.TradeMessage;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentReceivedMessage {
public SellerSendPaymentReceivedMessageToBuyer(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
protected NodeAddress getReceiverNodeAddress() {
return trade.getBuyer().getNodeAddress();
}
protected PubKeyRing getReceiverPubKeyRing() {
return trade.getBuyer().getPubKeyRing();
}
// continue execution on fault so payment received message is sent to arbitrator
@Override
protected void onFault(String errorMessage, TradeMessage message) {
setStateFault();
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
complete();
}
}

View file

@ -18,23 +18,25 @@
package bisq.core.trade.protocol.tasks; package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentAccountKeyResponse; import bisq.core.trade.messages.DepositsConfirmedMessage;
import bisq.core.trade.messages.TradeMailboxMessage; import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.common.app.Version; import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
/** /**
* Allow sender's payment account info to be decrypted when trade state is confirmed. * Send message on first confirmation to decrypt peer payment account and update multisig hex.
*/ */
@Slf4j @Slf4j
public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask { public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask {
private PaymentAccountKeyResponse message; private DepositsConfirmedMessage message;
public SellerSendPaymentAccountPayloadKey(TaskRunner<Trade> taskHandler, Trade trade) { public SendDepositsConfirmedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
} }
@ -47,16 +49,22 @@ public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask {
failed(t); failed(t);
} }
} }
@Override
protected abstract NodeAddress getReceiverNodeAddress();
@Override
protected abstract PubKeyRing getReceiverPubKeyRing();
@Override @Override
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
if (message == null) { if (message == null) {
// get updated multisig hex // export multisig hex once
if (trade.getSelf().getUpdatedMultisigHex() == null) { if (trade.getSelf().getUpdatedMultisigHex() == null) {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId); MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); // only export multisig hex once trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
} }
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the // We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
@ -64,13 +72,12 @@ public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask {
// messages where only the one which gets processed by the peer would be removed we use the same uid. All // messages where only the one which gets processed by the peer would be removed we use the same uid. All
// other data stays the same when we re-send the message at any time later. // other data stays the same when we re-send the message at any time later.
String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress(); String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress();
message = new PaymentAccountKeyResponse( message = new DepositsConfirmedMessage(
trade.getOffer().getId(), trade.getOffer().getId(),
processModel.getMyNodeAddress(), processModel.getMyNodeAddress(),
processModel.getPubKeyRing(), processModel.getPubKeyRing(),
deterministicId, deterministicId,
Version.getP2PMessageVersion(), getReceiverNodeAddress().equals(trade.getBuyer().getNodeAddress()) ? trade.getSeller().getPaymentAccountKey() : null, // buyer receives seller's payment account decryption key
trade.getSelf().getPaymentAccountKey(),
trade.getSelf().getUpdatedMultisigHex()); trade.getSelf().getUpdatedMultisigHex());
} }
return message; return message;

View file

@ -0,0 +1,46 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
/**
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
*/
@Slf4j
public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfirmedMessage {
public SendDepositsConfirmedMessageToArbitrator(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
public NodeAddress getReceiverNodeAddress() {
return trade.getArbitrator().getNodeAddress();
}
@Override
public PubKeyRing getReceiverPubKeyRing() {
return trade.getArbitrator().getPubKeyRing();
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
/**
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
*/
@Slf4j
public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMessage {
public SendDepositsConfirmedMessageToBuyer(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
public NodeAddress getReceiverNodeAddress() {
return trade.getBuyer().getNodeAddress();
}
@Override
public PubKeyRing getReceiverPubKeyRing() {
return trade.getBuyer().getPubKeyRing();
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.trade.Trade;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
/**
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
*/
@Slf4j
public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedMessage {
public SendDepositsConfirmedMessageToSeller(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
public NodeAddress getReceiverNodeAddress() {
return trade.getSeller().getNodeAddress();
}
@Override
public PubKeyRing getReceiverPubKeyRing() {
return trade.getSeller().getPubKeyRing();
}
}

View file

@ -1,66 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.common.UserThread;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
@Slf4j
public class SetupPayoutTxListener extends TradeTask {
private Subscription tradeStateSubscription;
@SuppressWarnings({ "unused" })
public SetupPayoutTxListener(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// skip if payout already published
if (!trade.isPayoutPublished()) {
// listen for payout tx
trade.listenForPayoutTx();
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> {
if (trade.isPayoutPublished()) {
// cleanup on trade completion
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId());
UserThread.execute(this::unSubscribe); // unsubscribe
}
});
}
complete();
} catch (Throwable t) {
failed(t);
}
}
private void unSubscribe() {
if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe();
}
}

View file

@ -1,116 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.messages.UpdateMultisigRequest;
import bisq.core.trade.messages.UpdateMultisigResponse;
import bisq.core.trade.protocol.TradeListener;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import bisq.common.app.Version;
import bisq.common.taskrunner.TaskRunner;
import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
@Slf4j
public class UpdateMultisigWithTradingPeer extends TradeTask {
private TradeListener updateMultisigResponseListener;
@SuppressWarnings({"unused"})
public UpdateMultisigWithTradingPeer(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// fetch relevant trade info
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerPreparesPaymentStartedMessage
// skip if multisig wallet does not need updated
if (!multisigWallet.isMultisigImportNeeded()) {
log.warn("Multisig wallet does not need updated, this should not happen");
failed();
return;
}
// register listener to receive updated multisig response
updateMultisigResponseListener = new TradeListener() {
@Override
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
if (!(message instanceof UpdateMultisigResponse)) return;
UpdateMultisigResponse response = (UpdateMultisigResponse) message;
multisigWallet.sync();
multisigWallet.importMultisigHex(response.getUpdatedMultisigHex());
trade.removeListener(updateMultisigResponseListener);
complete();
}
};
trade.addListener(updateMultisigResponseListener);
// get updated multisig hex
multisigWallet.sync();
String updatedMultisigHex = multisigWallet.exportMultisigHex();
// message trading peer with updated multisig hex
UpdateMultisigRequest message = new UpdateMultisigRequest(
processModel.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
updatedMultisigHex);
System.out.println("Sending message: " + message);
// TODO (woodser): trade.getTradingPeer().getNodeAddress() and/or trade.getTradingPeer().getPubKeyRing() are null on restart of application, so cannot send payment to complete trade
log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid(), trade.getTradingPeer().getNodeAddress());
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), message, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at trading peer: offerId={}; uid={}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", message.getClass().getSimpleName(), message.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
failed();
}
});
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -31,8 +31,8 @@ import bisq.proto.grpc.GetTradeReply;
import bisq.proto.grpc.GetTradeRequest; import bisq.proto.grpc.GetTradeRequest;
import bisq.proto.grpc.GetTradesReply; import bisq.proto.grpc.GetTradesReply;
import bisq.proto.grpc.GetTradesRequest; import bisq.proto.grpc.GetTradesRequest;
import bisq.proto.grpc.KeepFundsReply; import bisq.proto.grpc.CompleteTradeReply;
import bisq.proto.grpc.KeepFundsRequest; import bisq.proto.grpc.CompleteTradeRequest;
import bisq.proto.grpc.SendChatMessageReply; import bisq.proto.grpc.SendChatMessageReply;
import bisq.proto.grpc.SendChatMessageRequest; import bisq.proto.grpc.SendChatMessageRequest;
import bisq.proto.grpc.TakeOfferReply; import bisq.proto.grpc.TakeOfferReply;
@ -176,13 +176,13 @@ class GrpcTradesService extends TradesImplBase {
} }
} }
// TODO: rename KeepFundsRequest to CloseTradeRequest // TODO: rename CompleteTradeRequest to CloseTradeRequest
@Override @Override
public void keepFunds(KeepFundsRequest req, public void completeTrade(CompleteTradeRequest req,
StreamObserver<KeepFundsReply> responseObserver) { StreamObserver<CompleteTradeReply> responseObserver) {
try { try {
coreApi.closeTrade(req.getTradeId()); coreApi.closeTrade(req.getTradeId());
var reply = KeepFundsReply.newBuilder().build(); var reply = CompleteTradeReply.newBuilder().build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); responseObserver.onCompleted();
} catch (Throwable cause) { } catch (Throwable cause) {
@ -244,12 +244,12 @@ class GrpcTradesService extends TradesImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{ new HashMap<>() {{
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));

View file

@ -415,7 +415,7 @@ class GrpcWalletsService extends WalletsImplBase {
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
new HashMap<>() {{ new HashMap<>() {{
put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(50, SECONDS)); put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(100, SECONDS)); // TODO: why do tests make so many calls to get balances?
put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS)); put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
put(getSendBtcMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES)); put(getSendBtcMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));

View file

@ -28,7 +28,7 @@ import bisq.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
import bisq.core.offer.placeoffer.tasks.ValidateOffer; import bisq.core.offer.placeoffer.tasks.ValidateOffer;
import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage; import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage; import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
import bisq.core.trade.protocol.tasks.MakerSetLockTime; import bisq.core.trade.protocol.tasks.MakerSetLockTime;
import bisq.core.trade.protocol.tasks.RemoveOffer; import bisq.core.trade.protocol.tasks.RemoveOffer;
@ -36,8 +36,7 @@ import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage; import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.SellerPublishDepositTx; import bisq.core.trade.protocol.tasks.SellerPublishDepositTx;
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics; import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage; import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener;
import bisq.core.trade.protocol.tasks.TakerVerifyMakerFeePayment; import bisq.core.trade.protocol.tasks.TakerVerifyMakerFeePayment;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.common.taskrunner.Task; import bisq.common.taskrunner.Task;
@ -109,7 +108,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
TakerVerifyMakerFeePayment.class, TakerVerifyMakerFeePayment.class,
SellerPreparePaymentReceivedMessage.class, SellerPreparePaymentReceivedMessage.class,
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
SellerSendPaymentReceivedMessage.class SellerSendPaymentReceivedMessageToBuyer.class
) )
)); ));
@ -123,10 +122,9 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class, ApplyFilter.class,
BuyerPreparePaymentSentMessage.class, BuyerPreparePaymentSentMessage.class,
SetupPayoutTxListener.class,
BuyerSendPaymentSentMessage.class, BuyerSendPaymentSentMessage.class,
BuyerProcessPaymentReceivedMessage.class ProcessPaymentReceivedMessage.class
) )
)); ));
@ -142,10 +140,9 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class, ApplyFilter.class,
TakerVerifyMakerFeePayment.class, TakerVerifyMakerFeePayment.class,
BuyerPreparePaymentSentMessage.class, BuyerPreparePaymentSentMessage.class,
SetupPayoutTxListener.class,
BuyerSendPaymentSentMessage.class, BuyerSendPaymentSentMessage.class,
BuyerProcessPaymentReceivedMessage.class) ProcessPaymentReceivedMessage.class)
)); ));
addGroup("SellerAsMakerProtocol", addGroup("SellerAsMakerProtocol",
FXCollections.observableArrayList(Arrays.asList( FXCollections.observableArrayList(Arrays.asList(
@ -166,7 +163,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class, ApplyFilter.class,
SellerPreparePaymentReceivedMessage.class, SellerPreparePaymentReceivedMessage.class,
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view? //SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
SellerSendPaymentReceivedMessage.class SellerSendPaymentReceivedMessageToBuyer.class
) )
)); ));
} }

View file

@ -42,7 +42,7 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferDirection;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService; import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.TradeUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.util.VolumeUtil; import bisq.core.util.VolumeUtil;

View file

@ -424,8 +424,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
case PAYMENT_RECEIVED: case PAYMENT_RECEIVED:
appendMsg = Res.get("takeOffer.error.depositPublished"); appendMsg = Res.get("takeOffer.error.depositPublished");
break; break;
case PAYOUT_PUBLISHED: case COMPLETED:
case WITHDRAWN:
appendMsg = Res.get("takeOffer.error.payoutPublished"); appendMsg = Res.get("takeOffer.error.payoutPublished");
break; break;
default: default:
@ -444,7 +443,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
} }
private void applyTradeState() { private void applyTradeState() {
if (trade.isTakerFeePublished()) { if (trade.isDepositRequested()) {
if (takeOfferResultHandler != null) if (takeOfferResultHandler != null)
takeOfferResultHandler.run(); takeOfferResultHandler.run();

View file

@ -183,12 +183,11 @@ public class NotificationCenter {
private void onTradePhaseChanged(Trade trade, Trade.Phase phase) { private void onTradePhaseChanged(Trade trade, Trade.Phase phase) {
String message = null; String message = null;
if (trade.isPayoutPublished() && !trade.isWithdrawn()) { if (trade.isPayoutPublished() && !trade.isCompleted()) {
message = Res.get("notification.trade.completed"); message = Res.get("notification.trade.completed");
} else { } else {
if (trade instanceof MakerTrade && if (trade instanceof MakerTrade &&
phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal() || phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal()) {
phase.ordinal() == Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
final String role = trade instanceof BuyerTrade ? Res.get("shared.seller") : Res.get("shared.buyer"); final String role = trade instanceof BuyerTrade ? Res.get("shared.seller") : Res.get("shared.buyer");
message = Res.get("notification.trade.accepted", role); message = Res.get("notification.trade.accepted", role);
} }

View file

@ -590,7 +590,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Button cancelButton = tuple.second; Button cancelButton = tuple.second;
closeTicketButton.setOnAction(e -> { closeTicketButton.setOnAction(e -> {
disputesService.resolveDisputePayout(dispute, disputeResult, contract); disputesService.applyDisputePayout(dispute, disputeResult, contract);
doClose(closeTicketButton); doClose(closeTicketButton);
// if (dispute.getDepositTxSerialized() == null) { // if (dispute.getDepositTxSerialized() == null) {

View file

@ -200,7 +200,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentStarted(resultHandler, errorMessageHandler); ((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentStarted(resultHandler, errorMessageHandler);
} }
public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Trade trade = getTrade(); Trade trade = getTrade();
checkNotNull(trade, "trade must not be null"); checkNotNull(trade, "trade must not be null");
checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade"); checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade");
@ -466,7 +466,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
String payoutTxHashAsString = null; String payoutTxHashAsString = null;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.exportMultisigHex(); String updatedMultisigHex = multisigWallet.exportMultisigHex();
xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet
if (trade.getPayoutTxId() != null) { if (trade.getPayoutTxId() != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString(); // payoutTxHashAsString = payoutTx.getHashAsString();

View file

@ -30,8 +30,11 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferUtil; import bisq.core.offer.OfferUtil;
import bisq.core.provider.fee.FeeService; import bisq.core.provider.fee.FeeService;
import bisq.core.provider.mempool.MempoolService; import bisq.core.provider.mempool.MempoolService;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.ClosedTradableManager; import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtil; import bisq.core.trade.TradeUtil;
import bisq.core.user.User; import bisq.core.user.User;
@ -115,6 +118,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
@Getter @Getter
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private Subscription tradeStateSubscription; private Subscription tradeStateSubscription;
private Subscription payoutStateSubscription;
private Subscription messageStateSubscription; private Subscription messageStateSubscription;
@Getter @Getter
protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty(); protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty();
@ -160,6 +164,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = null; tradeStateSubscription = null;
} }
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
payoutStateSubscription = null;
}
if (messageStateSubscription != null) { if (messageStateSubscription != null) {
messageStateSubscription.unsubscribe(); messageStateSubscription.unsubscribe();
messageStateSubscription = null; messageStateSubscription = null;
@ -174,6 +183,12 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
buyerState.set(BuyerState.UNDEFINED); buyerState.set(BuyerState.UNDEFINED);
} }
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
sellerState.set(SellerState.UNDEFINED);
buyerState.set(BuyerState.UNDEFINED);
}
if (messageStateSubscription != null) { if (messageStateSubscription != null) {
messageStateSubscription.unsubscribe(); messageStateSubscription.unsubscribe();
messageStateProperty.set(MessageState.UNDEFINED); messageStateProperty.set(MessageState.UNDEFINED);
@ -184,6 +199,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> { tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
UserThread.execute(() -> onTradeStateChanged(state)); UserThread.execute(() -> onTradeStateChanged(state));
}); });
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
UserThread.execute(() -> onPayoutStateChanged(state));
});
messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentStartedMessageStateProperty(), this::onMessageStateChanged); messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentStartedMessageStateProperty(), this::onMessageStateChanged);
} }
} }
@ -399,6 +417,13 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeState, tradeState,
trade != null ? trade.getShortId() : "trade is null"); trade != null ? trade.getShortId() : "trade is null");
// arbitrator trade view only shows tx status
if (trade instanceof ArbitratorTrade) {
buyerState.set(BuyerState.STEP1);
sellerState.set(SellerState.STEP1);
return;
}
switch (tradeState) { switch (tradeState) {
// preparation // preparation
case PREPARATION: case PREPARATION:
@ -414,9 +439,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
// deposit requested // deposit requested
case SENT_PUBLISH_DEPOSIT_TX_REQUEST: case SENT_PUBLISH_DEPOSIT_TX_REQUEST:
case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST:
case STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST:
case SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST: case SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST:
case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST:
// deposit published // deposit published
case ARBITRATOR_PUBLISHED_DEPOSIT_TXS: case ARBITRATOR_PUBLISHED_DEPOSIT_TXS:
@ -456,29 +480,16 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
// seller step 4 // seller step 4
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: // UI action case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: // UI action
case SELLER_SENT_PAYMENT_RECEIVED_MSG: case SELLER_SENT_PAYMENT_RECEIVED_MSG:
case SELLER_PUBLISHED_PAYOUT_TX: // payout tx broadcasted if (trade instanceof BuyerTrade) buyerState.set(BuyerState.STEP4);
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG sent else if (trade instanceof SellerTrade) sellerState.set(SellerState.STEP3);
sellerState.set(SellerState.STEP3);
break; break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG: case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG: case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG arrived
case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG mailbox
case SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG failed - payout tx is published, peer will see it in network so we ignore failure and complete
sellerState.set(SellerState.STEP4); sellerState.set(SellerState.STEP4);
break; break;
// buyer step 4 case TRADE_COMPLETED:
case BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG:
// Alternatively the maker could have seen the payout tx earlier before he received the PAYOUT_TX_PUBLISHED_MSG:
case PAYOUT_TX_SEEN_IN_NETWORK:
// Alternatively the buyer could fully sign and publish the payout tx
case BUYER_PUBLISHED_PAYOUT_TX:
buyerState.set(BuyerState.STEP4);
break;
case WITHDRAW_COMPLETED:
sellerState.set(UNDEFINED); sellerState.set(UNDEFINED);
buyerState.set(BuyerState.UNDEFINED); buyerState.set(BuyerState.UNDEFINED);
break; break;
@ -491,4 +502,21 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
break; break;
} }
} }
private void onPayoutStateChanged(Trade.PayoutState payoutState) {
log.info("UI payoutState={}, id={}",
payoutState,
trade != null ? trade.getShortId() : "trade is null");
if (trade instanceof ArbitratorTrade) return;
switch (payoutState) {
case PUBLISHED:
sellerState.set(SellerState.STEP4);
buyerState.set(BuyerState.STEP4);
break;
default:
break;
}
}
} }

View file

@ -123,8 +123,6 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.play(); busyAnimation.play();
statusLabel.setText(Res.get("Confirming payment received. This can take up to a few minutes. Please wait...")); statusLabel.setText(Res.get("Confirming payment received. This can take up to a few minutes. Please wait..."));
break; break;
case SELLER_PUBLISHED_PAYOUT_TX:
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG:
case SELLER_SENT_PAYMENT_RECEIVED_MSG: case SELLER_SENT_PAYMENT_RECEIVED_MSG:
busyAnimation.play(); busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation")); statusLabel.setText(Res.get("shared.sendingConfirmation"));
@ -135,16 +133,14 @@ public class SellerStep3View extends TradeStepView {
}, 10); }, 10);
break; break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG:
busyAnimation.stop(); busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived")); statusLabel.setText(Res.get("shared.messageArrived"));
break; break;
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG: case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG:
busyAnimation.stop(); busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageStoredInMailbox")); statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
break; break;
case SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG: case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
// We get a popup and the trade closed, so we dont need to show anything here // We get a popup and the trade closed, so we dont need to show anything here
busyAnimation.stop(); busyAnimation.stop();
statusLabel.setText(""); statusLabel.setText("");
@ -464,7 +460,7 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.play(); busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation")); statusLabel.setText(Res.get("shared.sendingConfirmation"));
model.dataModel.onFiatPaymentReceived(() -> { model.dataModel.onPaymentReceived(() -> {
}, errorMessage -> { }, errorMessage -> {
busyAnimation.stop(); busyAnimation.stop();
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show(); new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show();

View file

@ -731,7 +731,7 @@ service Trades {
} }
rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) { rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) {
} }
rpc KeepFunds (KeepFundsRequest) returns (KeepFundsReply) { rpc CompleteTrade (CompleteTradeRequest) returns (CompleteTradeReply) {
} }
rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) { rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) {
} }
@ -787,11 +787,11 @@ message GetTradesReply {
repeated TradeInfo trades = 1; repeated TradeInfo trades = 1;
} }
message KeepFundsRequest { message CompleteTradeRequest {
string trade_id = 1; string trade_id = 1;
} }
message KeepFundsReply { message CompleteTradeReply {
} }
message WithdrawFundsRequest { message WithdrawFundsRequest {
@ -837,15 +837,16 @@ message TradeInfo {
string state = 16; string state = 16;
string phase = 17; string phase = 17;
string period_state = 18; string period_state = 18;
bool is_deposit_published = 19; string payout_state = 19;
bool is_deposit_unlocked = 20; bool is_deposit_published = 20;
bool is_payment_sent = 21; bool is_deposit_unlocked = 21;
bool is_payment_received = 22; bool is_payment_sent = 22;
bool is_payout_published = 23; bool is_payment_received = 23;
bool is_completed = 24; bool is_payout_published = 24;
string contract_as_json = 25; bool is_completed = 25;
ContractInfo contract = 26; string contract_as_json = 26;
string trade_volume = 27; ContractInfo contract = 27;
string trade_volume = 28;
string maker_deposit_tx_id = 100; string maker_deposit_tx_id = 100;
string taker_deposit_tx_id = 101; string taker_deposit_tx_id = 101;

View file

@ -74,17 +74,11 @@ message NetworkEnvelope {
SignContractResponse sign_contract_response = 1006; SignContractResponse sign_contract_response = 1006;
DepositRequest deposit_request = 1007; DepositRequest deposit_request = 1007;
DepositResponse deposit_response = 1008; DepositResponse deposit_response = 1008;
PaymentAccountKeyRequest payment_account_key_request = 1009; DepositsConfirmedMessage deposits_confirmed_message = 1009;
PaymentAccountKeyResponse payment_account_key_response = 1010; PaymentSentMessage payment_sent_message = 1010;
PaymentSentMessage payment_sent_message = 1011; PaymentReceivedMessage payment_received_message = 1011;
PaymentReceivedMessage payment_received_message = 1012; ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012;
PayoutTxPublishedMessage payout_tx_published_message = 1013; ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013;
ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1016;
ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1017;
// TODO: delete these
UpdateMultisigRequest update_multisig_request = 1018;
UpdateMultisigResponse update_multisig_response = 1019;
} }
} }
@ -355,37 +349,12 @@ message DepositResponse {
int64 current_date = 5; int64 current_date = 5;
} }
message PaymentAccountKeyRequest { message DepositsConfirmedMessage {
string trade_id = 1; string trade_id = 1;
NodeAddress sender_node_address = 2; NodeAddress sender_node_address = 2;
PubKeyRing pub_key_ring = 3; PubKeyRing pub_key_ring = 3;
string uid = 4; string uid = 4;
} bytes seller_payment_account_key = 5;
message PaymentAccountKeyResponse {
string trade_id = 1;
NodeAddress sender_node_address = 2;
PubKeyRing pub_key_ring = 3;
string uid = 4;
bytes payment_account_key = 5;
string updated_multisig_hex = 6;
}
message UpdateMultisigRequest {
string trade_id = 1;
NodeAddress sender_node_address = 2;
PubKeyRing pub_key_ring = 3;
string uid = 4;
int64 current_date = 5;
string updated_multisig_hex = 6;
}
message UpdateMultisigResponse {
string trade_id = 1;
NodeAddress sender_node_address = 2;
PubKeyRing pub_key_ring = 3;
string uid = 4;
int64 current_date = 5;
string updated_multisig_hex = 6; string updated_multisig_hex = 6;
} }
@ -454,16 +423,10 @@ message PaymentReceivedMessage {
NodeAddress sender_node_address = 2; NodeAddress sender_node_address = 2;
string uid = 3; string uid = 3;
SignedWitness signed_witness = 4; // Added in v1.4.0 SignedWitness signed_witness = 4; // Added in v1.4.0
string payout_tx_hex = 5; string unsigned_payout_tx_hex = 5;
}
message PayoutTxPublishedMessage {
string trade_id = 1;
NodeAddress sender_node_address = 2;
bool is_maker = 3;
string uid = 4;
SignedWitness signed_witness = 5;
string signed_payout_tx_hex = 6; string signed_payout_tx_hex = 6;
string updated_multisig_hex = 7;
bool saw_arrived_payment_received_msg = 8;
} }
message ArbitratorPayoutTxRequest { message ArbitratorPayoutTxRequest {
@ -1644,33 +1607,24 @@ message Trade {
CONTRACT_SIGNATURE_REQUESTED = 6; CONTRACT_SIGNATURE_REQUESTED = 6;
CONTRACT_SIGNED = 7; CONTRACT_SIGNED = 7;
SENT_PUBLISH_DEPOSIT_TX_REQUEST = 8; SENT_PUBLISH_DEPOSIT_TX_REQUEST = 8;
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 9; SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 9;
STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 10; SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 10;
SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 11; ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 11;
ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 12; DEPOSIT_TXS_SEEN_IN_NETWORK = 12;
DEPOSIT_TXS_SEEN_IN_NETWORK = 13; DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 13;
DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 14; DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 14;
DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 15; BUYER_CONFIRMED_IN_UI_PAYMENT_SENT = 15;
BUYER_CONFIRMED_IN_UI_PAYMENT_SENT = 16; BUYER_SENT_PAYMENT_SENT_MSG = 16;
BUYER_SENT_PAYMENT_SENT_MSG = 17; BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 17;
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 18; BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 18;
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 19; BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 19;
BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 20; SELLER_RECEIVED_PAYMENT_SENT_MSG = 20;
SELLER_RECEIVED_PAYMENT_SENT_MSG = 21; SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 21;
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 22; SELLER_SENT_PAYMENT_RECEIVED_MSG = 22;
SELLER_SENT_PAYMENT_RECEIVED_MSG = 23; SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 23;
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 24; SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 24;
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25; SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 25;
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 26; TRADE_COMPLETED = 26;
SELLER_PUBLISHED_PAYOUT_TX = 27;
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 28;
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 29;
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 30;
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 31;
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 32;
BUYER_PUBLISHED_PAYOUT_TX = 33;
PAYOUT_TX_SEEN_IN_NETWORK = 34;
WITHDRAW_COMPLETED = 35;
} }
enum Phase { enum Phase {
@ -1682,8 +1636,14 @@ message Trade {
DEPOSITS_UNLOCKED = 5; DEPOSITS_UNLOCKED = 5;
PAYMENT_SENT = 6; PAYMENT_SENT = 6;
PAYMENT_RECEIVED = 7; PAYMENT_RECEIVED = 7;
PAYOUT_PUBLISHED = 8; COMPLETED = 8;
WITHDRAWN = 9; }
enum PayoutState {
UNPUBLISHED = 0;
PUBLISHED = 1;
CONFIRMED = 2;
UNLOCKED = 3;
} }
enum DisputeState { enum DisputeState {
@ -1718,23 +1678,24 @@ message Trade {
int64 take_offer_date = 9; int64 take_offer_date = 9;
int64 price = 10; int64 price = 10;
State state = 11; State state = 11;
DisputeState dispute_state = 12; PayoutState payout_state = 12;
TradePeriodState period_state = 13; DisputeState dispute_state = 13;
Contract contract = 14; TradePeriodState period_state = 14;
string contract_as_json = 15; Contract contract = 15;
bytes contract_hash = 16; string contract_as_json = 16;
NodeAddress arbitrator_node_address = 17; bytes contract_hash = 17;
NodeAddress mediator_node_address = 18; NodeAddress arbitrator_node_address = 18;
string error_message = 19; NodeAddress mediator_node_address = 19;
string counter_currency_tx_id = 20; string error_message = 20;
repeated ChatMessage chat_message = 21; string counter_currency_tx_id = 21;
MediationResultState mediation_result_state = 22; repeated ChatMessage chat_message = 22;
int64 lock_time = 23; MediationResultState mediation_result_state = 23;
NodeAddress refund_agent_node_address = 24; int64 lock_time = 24;
RefundResultState refund_result_state = 25; NodeAddress refund_agent_node_address = 25;
string counter_currency_extra_data = 26; RefundResultState refund_result_state = 26;
string asset_tx_proof_result = 27; // name of AssetTxProofResult enum string counter_currency_extra_data = 27;
string uid = 28; string asset_tx_proof_result = 28; // name of AssetTxProofResult enum
string uid = 29;
} }
message BuyerAsMakerTrade { message BuyerAsMakerTrade {